+ Start a Discussion
ggarciaggarcia 

<canvas> html5 element in visual force

Hey guys I'm a little new to this and I need some help. I need to make a visual force page using html5 and the <canvas> element. The canvas will create a modifiable box in which a user can draw something and then that will be stored elsewhere as a JSON file to be recalled later. I need to know if and how this would be possible. I can get a box to show up but I can't get it to be editable ( I can't draw inside of it). 

 

HELP PLEASE 

Best Answer chosen by Admin (Salesforce Developers) 
sfdcfoxsfdcfox
public with sharing class captureHelper {

    public captureHelper(ApexPages.StandardController controller) {

    }

    @RemoteAction public static RemoteSaveResult saveFile(Id accountId, String imageData) {
        Attachment record = new Attachment(ParentId=accountId, name='Signature.png', ContentType='image/png', Body=EncodingUtil.base64Decode(imageData));
        Database.saveResult result = Database.insert(record,false);
        RemoteSaveResult newResult = new RemoteSaveResult();
        newResult.success = result.isSuccess();
        newResult.attachmentId = record.Id;
        newResult.errorMessage = result.isSuccess()?'':result.getErrors()[0].getMessage();
        return newResult;
    }
    
    public class RemoteSaveResult {
        public Boolean success;
        public Id attachmentId;
        public String errorMessage;
    }
}

 

<apex:page docType="html-5.0" standardController="Account" extensions="captureHelper">
    <canvas id="demo" width="250" height="250"/>
    <apex:includeScript value="/soap/ajax/28.0/connection.js"/>
    <script>
    sforce.connection.sessionId = "{!$Api.Session_Id}";
    var canvas = document.getElementById("demo");
    var context = canvas.getContext("2d");
    var mouseButton = 0;
    var lastX = lastY = null;

    function saveImage() {
        var image = canvas.toDataURL().split(',')[1];
        var result = captureHelper.saveFile('{!Account.Id}','image',handleResult);
    }
    
    function handleResult(result,event) {
        if(result.success) {
            alert('Successfully saved image');
        } else {
            alert('Error: '+result.errorMessage);
        }
    }
    
    function handleEvent(event) {
        if(event.type=="mousedown") {
            mouseButton = event.which;
            lastX = lastY = null;
        }
        if(event.type=="mouseup") {
            mouseButton = 0;
            lastX = lastY = null;
        }
        if(event.type=="mousemove" && mouseButton) {
            if(!lastX && !lastY) {
                lastX = event.offsetX;
                lastY = event.offsetY;
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(lastX,lastY,lastX,lastY);
                context.stroke();
            } else {
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(event.offsetX,event.offsetY);
                context.stroke();
                lastX = event.offsetX;
                lastY = event.offsetY;
            }
        }
        if(event.type=="mousedrag") {
            event.returnValue=false;
            event.stopPropagation();
        }
    }
    
    canvas.addEventListener("mousemove",handleEvent,true);
    window.addEventListener("mousedown",handleEvent,true);
    window.addEventListener("mouseup",handleEvent,true);
    canvas.addEventListener("mousedrag",handleEvent,true);
    canvas.addEventListener("selectstart",function(event) { event.stopPropagation(); event.returnValue = false; return false; },false);
    </script>
    <button onclick="saveImage()">Save</button>
</apex:page>

This works on account; adjust to suit your needs.

All Answers

sfdcfoxsfdcfox
<apex:page docType="html-5.0">
    <canvas id="demo" width="250" height="250"/>
    <script>
    var canvas = document.getElementById("demo");
    var context = canvas.getContext("2d");
    var mouseButton = 0;
    var lastX = lastY = null;

    function handleEvent(event) {
        if(event.type=="mousedown") {
            mouseButton = event.which;
            lastX = lastY = null;
        }
        if(event.type=="mouseup") {
            mouseButton = 0;
            lastX = lastY = null;
        }
        if(event.type=="mousemove" && mouseButton) {
            if(!lastX && !lastY) {
                lastX = event.offsetX;
                lastY = event.offsetY;
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(lastX,lastY,lastX,lastY);
                context.stroke();
            } else {
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(event.offsetX,event.offsetY);
                context.stroke();
                lastX = event.offsetX;
                lastY = event.offsetY;
            }
        }
        if(event.type=="mousedrag") {
            event.returnValue=false;
            event.stopPropagation();
        }
    }
    
    canvas.addEventListener("mousemove",handleEvent,true);
	window.addEventListener("mousedown",handleEvent,true);
    window.addEventListener("mouseup",handleEvent,true);
    canvas.addEventListener("mousedrag",handleEvent,true);
    canvas.addEventListener("selectstart",function(event) { event.stopPropagation(); event.returnValue = false; return false; },false);
    </script>
</apex:page>

This is just an example. It works verbatim in my Developer Org. Feel free to use a framework instead of writing your own code (and this code isn't cross-browser by any means).

ggarciaggarcia

this works, it gives me a box in which to draw just like i want but how to i capture that as an image and save it as an attachment? 

Cory CowgillCory Cowgill

Check out my tutorial video here which shows how to use HTML5 Canvas and Salesforce REST API to do this.

 

http://www.youtube.com/watch?v=1KILol_pa84

 

This is not in a Visualforce Page, but you could simply copy the HTML code and JavaScript into the VF Page to get it to work.

sfdcfoxsfdcfox
<apex:page docType="html-5.0">
    <canvas id="demo" width="250" height="250"/>
    <apex:includeScript value="/soap/ajax/28.0/connection.js"/>
    <script>
    sforce.connection.sessionId = "{!$Api.Session_Id}";
    var canvas = document.getElementById("demo");
    var context = canvas.getContext("2d");
    var mouseButton = 0;
    var lastX = lastY = null;

    function saveImage() {
        var image = canvas.toDataURL().split(',')[1];
        var doc = new sforce.SObject("Document");
        doc.FolderId = "{!$User.Id}";
        doc.ContentType = "image/png";
        doc.Body = image;
        doc.Name = "my image.png";
        sforce.connection.create([doc],{onSuccess:result, onFailure:result});
    }
    
    function result(value) {
        console.debug(value);
		if(value.success==="true") {
            alert("Successfully saved drawing to My Documents");
        } else {
            alert("Error saving file: "+value[0].errors.message);
        }	
    }
    
    function handleEvent(event) {
        if(event.type=="mousedown") {
            mouseButton = event.which;
            lastX = lastY = null;
        }
        if(event.type=="mouseup") {
            mouseButton = 0;
            lastX = lastY = null;
        }
        if(event.type=="mousemove" && mouseButton) {
            if(!lastX && !lastY) {
                lastX = event.offsetX;
                lastY = event.offsetY;
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(lastX,lastY,lastX,lastY);
                context.stroke();
            } else {
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(event.offsetX,event.offsetY);
                context.stroke();
                lastX = event.offsetX;
                lastY = event.offsetY;
            }
        }
        if(event.type=="mousedrag") {
            event.returnValue=false;
            event.stopPropagation();
        }
    }
    
    canvas.addEventListener("mousemove",handleEvent,true);
	window.addEventListener("mousedown",handleEvent,true);
    window.addEventListener("mouseup",handleEvent,true);
    canvas.addEventListener("mousedrag",handleEvent,true);
    canvas.addEventListener("selectstart",function(event) { event.stopPropagation(); event.returnValue = false; return false; },false);
    </script>
    <button onclick="saveImage()">Save</button>
</apex:page>

Added a "save" function that would work, so there is a complete example here.

ggarciaggarcia
you are awesome sir, so then i just have to make this be pretty and cross browser combatible and have it save to a certain object's attachment depending on which object requested this page and add in a text box for a name.

end goal here is have a pdf display a signature and name depending once the pdf is rendered off of an object

pretty much allowing a person to sign off on a purchase order and when the pdf is printed out it will display that signature at the bottom with the name of that person in text.
sfdcfoxsfdcfox
Glad I could be of service. Code should be compatible with IE 10, recent versions of Chrome, Opera, and Safari. I think the only thing you'll have to do is add a couple of shims for IE <10, and you should be good to go.
ggarciaggarcia

so what i did is i created a button within the object that i wish to store the attachment in and had it open up the page that you litterally created for me (thanks again) and now it wont work. it will open the page just like if i was typing it directly into the address bar but the canvas element wont work i even tried to make the button run it was java script window.open("URL") but that didn't work either

sfdcfoxsfdcfox
What errors are you getting in the debug console for your browser?
ggarciaggarcia

nevermind  i fixed it it was a simple syntax error. now onto adding the document you made to the object from which this visual force page was requested instead of to the documents tab 

ggarciaggarcia

or should i just find a way to name the file in the documents tab as it is now some name that will reference the object from which the canvas page was called..... this will eventually lead to a signature option litterally allowing someone to go into an object ( be it a purchase order or a sales order and sign for it , think UPS signing for packages type stuff) but now that signature needs to be saved somewhere to be recalled whenever someone asks who signed for this or when that object is being turned into a pdf document 

 

u seem very knowledgable and i was wondering what would be easier:

 

1) using javascript within the "sign" button i created to move the signature from documents to the object's notes and attachments tab, from there when the "print PDF" button is pressed it would jsut look for an attachment 

 

or 

 

2) leaving the signature file in the documents tab and naming it in some way that it would be easily recognizeable as to which object it belongs and use that in order to pull it onto the pdf without having to move it over to the object's attachments 

 

you have helped this young programer very much and i appriciate it if you could just help a little more it would be wonderful thank you 

 

 

P.S. personally i think option one is better bc then in the code to print the pdf i can just tell it to go to attachments and find a "my image/png" and not have it looking through all of the documents looking for a certain one named a certain thing

ggarciaggarcia

how would i change this to save as an attachment to the object that called this visual force page instead of to the documents tab ? 

 

spesifically to the one that called it

 

sfdcfoxsfdcfox
You'll just need to grab the ID from the controller or page parameters (your choice), then save it as an Attachment with ParentId set to the parent record.

Or, you could use Visualforce remoting to give it a more salesforce-y feel; simply have the remoteaction method accept a base64-encoded file and a parent ID, and have it commit the data for you (and save on API calls).
ggarciaggarcia

how do i do the remoting thing...i litterally have never done any apex before this project i kno im probably annoying the crap out of u sorry =/

ggarciaggarcia

i was also thinking of using ur exact page u gave me before to just save it directly into the PO that called that page in other words 

 

object has sign button on layout

user clicks sign button 

user taken to signing page (button just has redirect URL in it)

user signs and clicks save

when user saves it checks the page that called for the signature and saves as an attachment to that object

 

how would that be completed?

sfdcfoxsfdcfox
public with sharing class captureHelper {

    public captureHelper(ApexPages.StandardController controller) {

    }

    @RemoteAction public static RemoteSaveResult saveFile(Id accountId, String imageData) {
        Attachment record = new Attachment(ParentId=accountId, name='Signature.png', ContentType='image/png', Body=EncodingUtil.base64Decode(imageData));
        Database.saveResult result = Database.insert(record,false);
        RemoteSaveResult newResult = new RemoteSaveResult();
        newResult.success = result.isSuccess();
        newResult.attachmentId = record.Id;
        newResult.errorMessage = result.isSuccess()?'':result.getErrors()[0].getMessage();
        return newResult;
    }
    
    public class RemoteSaveResult {
        public Boolean success;
        public Id attachmentId;
        public String errorMessage;
    }
}

 

<apex:page docType="html-5.0" standardController="Account" extensions="captureHelper">
    <canvas id="demo" width="250" height="250"/>
    <apex:includeScript value="/soap/ajax/28.0/connection.js"/>
    <script>
    sforce.connection.sessionId = "{!$Api.Session_Id}";
    var canvas = document.getElementById("demo");
    var context = canvas.getContext("2d");
    var mouseButton = 0;
    var lastX = lastY = null;

    function saveImage() {
        var image = canvas.toDataURL().split(',')[1];
        var result = captureHelper.saveFile('{!Account.Id}','image',handleResult);
    }
    
    function handleResult(result,event) {
        if(result.success) {
            alert('Successfully saved image');
        } else {
            alert('Error: '+result.errorMessage);
        }
    }
    
    function handleEvent(event) {
        if(event.type=="mousedown") {
            mouseButton = event.which;
            lastX = lastY = null;
        }
        if(event.type=="mouseup") {
            mouseButton = 0;
            lastX = lastY = null;
        }
        if(event.type=="mousemove" && mouseButton) {
            if(!lastX && !lastY) {
                lastX = event.offsetX;
                lastY = event.offsetY;
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(lastX,lastY,lastX,lastY);
                context.stroke();
            } else {
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(event.offsetX,event.offsetY);
                context.stroke();
                lastX = event.offsetX;
                lastY = event.offsetY;
            }
        }
        if(event.type=="mousedrag") {
            event.returnValue=false;
            event.stopPropagation();
        }
    }
    
    canvas.addEventListener("mousemove",handleEvent,true);
    window.addEventListener("mousedown",handleEvent,true);
    window.addEventListener("mouseup",handleEvent,true);
    canvas.addEventListener("mousedrag",handleEvent,true);
    canvas.addEventListener("selectstart",function(event) { event.stopPropagation(); event.returnValue = false; return false; },false);
    </script>
    <button onclick="saveImage()">Save</button>
</apex:page>

This works on account; adjust to suit your needs.

This was selected as the best answer
ggarciaggarcia

i get this error 

 

Error: Required fields are missing: [Parent]

ggarciaggarcia

I fixed that error but now when I click on the "view" button on the attachment it doesn't show the image like it used to 

 

I also need it to be able to output that image onto a visualforce page that will be rendered as a PDF 

 

I'm assuming that to do this I would use 

 

<apex:outputfield> inside of the VF page that will be rendered as PDF except I can't figure out how I would reference the 'Signature.png' file through a specific object's attachments 

sfdcfoxsfdcfox

1) Not sure why it wouldn't view, it's working on my code. I updated the code (again) to account for the missing accountId:

 

<apex:page docType="html-5.0" standardController="Account" extensions="captureHelper">
    <canvas id="demo" width="250" height="250"/>
    <apex:includeScript value="/soap/ajax/28.0/connection.js"/>
    <script>
    sforce.connection.sessionId = "{!$Api.Session_Id}";
    var canvas = document.getElementById("demo");
    var context = canvas.getContext("2d");
    var mouseButton = 0;
    var lastX = lastY = null;
    var accountId = '{!Account.Id}';

    function saveImage() {

        var image = canvas.toDataURL().split(',')[1];
        captureHelper.saveFile(accountId,image,handleResult);
    }
    
    function handleResult(result,event) {
        if(result.success) {
            window.top.location.href='/'+accountId;
        } else {
            alert('Error: '+result.errorMessage);
        }
    }
    
    function handleEvent(event) {
        if(event.type=="mousedown") {
            mouseButton = event.which;
            lastX = lastY = null;
        }
        if(event.type=="mouseup") {
            mouseButton = 0;
            lastX = lastY = null;
        }
        if(event.type=="mousemove" && mouseButton) {
            if(!lastX && !lastY) {
                lastX = event.offsetX;
                lastY = event.offsetY;
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(lastX,lastY,lastX,lastY);
                context.stroke();
            } else {
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(event.offsetX,event.offsetY);
                context.stroke();
                lastX = event.offsetX;
                lastY = event.offsetY;
            }
        }
        if(event.type=="mousedrag") {
            event.returnValue=false;
            event.stopPropagation();
        }
    }
    
    canvas.addEventListener("mousemove",handleEvent,true);
    window.addEventListener("mousedown",handleEvent,true);
    window.addEventListener("mouseup",handleEvent,true);
    canvas.addEventListener("mousedrag",handleEvent,true);
    canvas.addEventListener("selectstart",function(event) { event.stopPropagation(); event.returnValue = false; return false; },false);
    </script>
    <button onclick="saveImage()">Save</button>
</apex:page>

2) Use an img tag to include it as an image:

 

<img src="/servlet/servlet.FileDownload?file=AttachmentId"/>

In order to get the attachment ID to the parent object, I'd probably recommend a normal text field to store the attachment ID, then you can simply reference the ID directly:

 

<img src="/servlet/servlet.FileDownload?file={!Parent.Signature__c}"/>

Your VF page controller could also query the ID, etc.

ggarciaggarcia

it doesnt wanna show up on the pdf all i get is this error:

 

Error: Unknown property 'PBSI__PBSI_Purchase_Order__cStandardController.Parent

 

 

when i use this 

<img src="/servlet/servlet.FileDownload?file={!Parent.Signature__c}"/>

 

if i take away parent i get a broken image link icon

sfdcfoxsfdcfox
I do apologize, "parent" isn't meant to be literal, it would be probably "relatedTo" or "PBSI__PBSI_Purchase_Order__c" (assuming you created the custom field as I suggested, and modified the controller code to update the custom object record).
ggarciaggarcia

ok i thought parent was a reference call 

ggarciaggarcia

i finally got it working in my browser and i would like to thank you very much. you have helped me a lot, however do u have any idea how to make this be browser independent and mobile friendly ? I can get the canvas element to show up (i put a stroke around it to make it easier to see) but i can not draw on it.

sfdcfoxsfdcfox

You just have to add touch events to allow mobile support. Apparently this works on about 82% of devices. Note that I had to fiddle with some of the values, so your milage may vary. You can increase support percentage by adding support for msPointerEvent support as well. I don't have time to do this today, but I may post a further example later when I get time. Note: at least on my Andriod, I had to hold my touch briefly until the canvas "flickered", and then I could draw on it. This is probably platform specific.

 

<apex:page docType="html-5.0" standardController="Account" extensions="captureHelper">
    <canvas id="demo" width="250" height="250"/>
    <apex:includeScript value="/soap/ajax/28.0/connection.js"/>
    <script>
    sforce.connection.sessionId = "{!$Api.Session_Id}";
    var canvas = document.getElementById("demo");
    var context = canvas.getContext("2d");
    var mouseButton = 0;
    var lastX = lastY = null;
    var accountId = '{!Account.Id}';

    function saveImage() {

        var image = canvas.toDataURL().split(',')[1];
        captureHelper.saveFile(accountId,image,handleResult);
    }
    
    function handleResult(result,event) {
        if(result.success) {
            window.top.location.href='/'+accountId;
        } else {
            alert('Error: '+result.errorMessage);
        }
    }
    
    function handleEvent(event) {
        if(event.type==="mousedown"||event.type==="touchstart") {
            mouseButton = event.which || 1;
            lastX = lastY = null;
        }
        if(event.type==="touchcancel" || event.type==="touchcancel" || event.type==="mouseup") {
            mouseButton = 0;
            lastX = lastY = null;
        }
        if((event.type==="touchmove" || event.type==="mousemove") && mouseButton) {
            var newX, newY;
            var canvasX = 0, canvasY = 0, obj = event.srcElement || event.target;
            do {
                canvasX += obj.offsetLeft;
                canvasY += obj.offsetTop;
            } while(obj = obj.offsetParent);
            if(event.targetTouches && event.targetTouches.length) {
                newX = event.targetTouches[0].clientX - (canvasX/2);
                newY = event.targetTouches[0].clientY - (canvasY/2);
            } else {
                newX = event.offsetX;
                newY = event.offsetY;
            }
            if(!lastX && !lastY) {
                lastX = newX;
                lastY = newY;
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(lastX,lastY,lastX,lastY);
                context.stroke();
            } else {
                context.beginPath();
                context.moveTo(lastX,lastY);
                context.lineTo(newX,newY);
                context.stroke();
                lastX = newX;
                lastY = newY;
            }
        }
        if(event.type=="touchmove" || event.type==="mousedrag" || (event.type==="selectstart" && (event.srcElement||event.target)===canvas)) {
            event.returnValue=false;
            event.stopPropagation();
            event.preventDefault();
            return false;
        }
    }

    canvas.addEventListener("mousedrag",handleEvent,true);
    canvas.addEventListener("mousemove",handleEvent,true);
    canvas.addEventListener("mousedown",handleEvent,true);
    window.addEventListener("mouseup",handleEvent,true);
    canvas.addEventListener("touchstart",handleEvent,true);
    canvas.addEventListener("touchmove",handleEvent,true);
    window.addEventListener("touchend",handleEvent,true);
    window.addEventListener("selectstart",handleEvent,true);
    </script>
    <button onclick="saveImage()">Save</button>
</apex:page>

 

Tyler OchsnerTyler Ochsner
How can I make this work on the Salesforce1 app for iOS? I have based my code off the latest code sample, but an attachment is only added when done through a desktop browser or Salesforce1 simulator. I am able to draw a signature on the VF page through the Salesforce1 app on an iPhone 5 running iOS 7, but the attachment isn't created for an unknown reason.  Do you have any ideas?