0 Replies - 1711 Views - Last Post: 07 September 2014 - 01:25 PM

#1 Blindman67  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 138
  • View blog
  • Posts: 615
  • Joined: 15-March 14

Translate, Rotate, Scale canvas patterns.

Posted 07 September 2014 - 01:25 PM

The 2D canvas API has a useful object for strokeStyle and fillStyle. Pattern is created via ctx.createPattern(image,"repeat"); that allows a pattern to be created from a image or another canvas. The problem is that the pattern is fixed, it can not be moved without alot of work.

Below I present a Object PatternHelper that will help moving a pattern around. I works by replacing most of the draw methods of the canvas 2D context. You can set the patterns origin (where in the pattern the pattern rotates). Then you can set where on the canvas the patterns origin will be, then you can set the scale, and rotation of the pattern.

To move the pattern I set the transform of the canvas to that needed for the pattern, then you call the draw function, and patternHelper inverts the transform so that you still draw in the standard transform.


Example
var ctx = canvasElement.getContext("2d"); 
var img = new Image();
img.src = "image.name";
var pattern = ctx.createPattern(img,"repeat")
var patHelper = new PatternHelper(ctx);  // create a new pattern

ctx.fillStyle = pattern; // set the fill style to the pattern

patHelper.origin(10,10);       // sets the pattern origin
patHelper.world(100,100);      // move that origin to pixel location 100 100
patHelper.rotation(Math.PI/4); // rotate clockwise 45 degrees
patHelper.scale(0.5);          // half the scale

ctx.beginPath();
patHelper.rect(20,20,160,160); // Set up the draw rectangle
ctx.fill();                    // now fill the rectangle with the adjusted pattern



I have included code to test and as example of its use. Works on all browsers that support HTML5 canvas. Some functions are not supported by some browsers, but the code will ignore calls if not supported.

I have added support for fillText and strokeText but it is limited. See the comments below for details.

Fell free to contact me if you have questions. If you find this useful please leave a comment or add a plus so I can see if my snippets are of use.

Thanks.



Code below shows how to use the PatternHelper
<!DOCTYPE html>
<html id="topDoc">
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-8">
        <title>Pattern Tester</title>
    </head>
<body>
    <!-- canvas to draw on -->
    <canvas id='can' width = 1024 height = 1024>Get your self a modern browser FFS!</canvas>
<script>
// Pattern helper test and example code.
// Globals for the test
var ctx;           // 2d context
var patternImage;  // pattern image
var ang = 0;       // ang (rotation) of pattern
var scale = 0;     // scale of pattern
var pat;           // ctx pattern object
var patHelper;     // Pattern helper object
var mouseX = 0;     // mouse position
var mouseY = 0;
var canWidth;       // canvas height and width
var canHeight;
//------------------------------------------------------------------------------------------------------
// wait for page to load then set up tests
window.addEventListener("load",function(){
        ctx=can.getContext("2d",{alpha:true});              // get canvas context
        canWidth = can.width;                               // get height and width
        canHeight = can.height;
        patternImage = new Image();                         // create an image
        patternImage.src = "g42681a.png";                   // point to a valid image URL
                                                            // Add your own image if needed
        patternImage.onload = drawPatterns;   // wait for the image to load;
        document.addEventListener('mousemove' , setMouse);  // setup mouse events          
    },false);
    
//------------------------------------------------------------------------------------------------------
// handles mouse move    
function setMouse(event){
    mouseX = event.offsetX; 
    mouseY = event.offsetY; 
    if(mouseX === undefined){ // if firefox
        mouseX = event.clientX;  // should be subtracting scroll position as mouse is relative on firefox.
        mouseY = event.clientY;
    }
}    
//------------------------------------------------------------------------------------------------------
// draw some patterns    
function drawPatterns(){
    if(pat === undefined){  // if no pattern set it now and create a PatternHelper
        pat = ctx.createPattern(patternImage,"repeat");
        try{
            patHelper = new PatternHelper(ctx);
        }catch(e){
            return;     // stops drawing if no 2d context available.
        }
        //---------------------------------------------------------
        // set the pattern origin
        patHelper.origin(patternImage.width/2,patternImage.height/2);
    }
    //------------------------------------------------------------------------------------------------------
    // clear the canvas 
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,1024,1024);
    
    // Set shadows to test shadows
    ctx.shadowBlur=20;
    ctx.shadowColor="black";
    ctx.shadowOffsetX = 20;
    ctx.shadowOffsetY = 20;
    
    // test line width and strokes
    ctx.strokeStyle = "black";
    ctx.lineWidth = 10;
    patHelper.lineWidth = ctx.lineWidth;  // set pattern helper line width. 
    
    // set the fillStyle to the pattern
    ctx.fillStyle = pat;
    
    // scale the pattern from 0.1 to 0.9
    patHelper.scale((Math.sin(ang*3)*0.4+0.5));
    
    // position the pattern origin to the canvas coords
    patHelper.world(canWidth/2,canHeight/2);
    
    // set the patterns rotation
    patHelper.rotation(ang);
    
    // draw a circle
    ctx.beginPath();
    patHelper.arc(canWidth/2,canHeight/2,120,0,2*Math.PI); // call patternHelpers mirror arc function..
    ctx.fill();  // fill it 

    //------------------------------------------------------------------------------------------------------
    // test vector method and check for thrown error
    patHelper.world(512+256,512);
    try{
        // set rotation and scale via vector
        patHelper.rotation(((512+256)-mouseX)/patternImage.width,(512-mouseY)/patternImage.height);
        ctx.beginPath();
        patHelper.arc(512+256,512,120,0,2*Math.PI); // call patternHelpers mirror arc function..
        ctx.fill();  // fill it 
    }catch(e){
        console.log("Mouse resulted in zero length vector")
    }
    
    
    // set up the scale and rotation 
    patHelper.scale((Math.sin(ang*3)*0.4+0.5));  
    patHelper.rotation(ang);
    
    // turn off the shadows
    ctx.shadowBlur=0;
    ctx.shadowColor="black";
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 0;


    
    //------------------------------------------------------------------------------------------------------
    // draw a rectangle with a stroke to make sure lineWidth works.
    patHelper.world(canWidth/4,canHeight/4);
    ctx.beginPath();
    patHelper.rect(canWidth/8,canHeight/8,canWidth/4,canHeight/4);
    ctx.fill();
    ctx.stroke();
    
    
    //------------------------------------------------------------------------------------------------------
    // draw a random bezier curve
    var steps = 4;
    var centerX = canWidth*0.75;
    var centerY = canHeight*0.25;
    patHelper.world(centerX,centerY);
    var r = (Math.random()-0.5)*40;
    var x = Math.sin(0)*(100+r);
    var y = Math.cos(0)*(100+r);
    ctx.beginPath();
    patHelper.moveTo(x+centerX,y+centerY);
    for(var i = 0 ; i < Math.PI*2; i+= Math.PI/steps){
        r = (Math.random()-0.5)*40;
        var x1 = Math.sin(i+Math.PI/steps/3)*(100+r);
        var y1 = Math.cos(i+Math.PI/steps/3)*(100+r);
        r = (Math.random()-0.5)*40;
        var x2 = Math.sin(i+Math.PI/steps/3*2)*(100+r);
        var y2 = Math.cos(i+Math.PI/steps/3*2)*(100+r);
        r = (Math.random()-0.5)*40;
        x = Math.sin(i+Math.PI/steps)*(100+r);
        y = Math.cos(i+Math.PI/steps)*(100+r);
        patHelper.bezierCurveTo(x1+centerX,y1+centerY,x2+centerX,y2+centerY,x+centerX,y+centerY);
    }
    ctx.fill();
    ctx.stroke();

    //------------------------------------------------------------------------------------------------------
    // draw a random quadratic curve
    var steps = 4;
    var centerX = canWidth*0.75;
    var centerY = canHeight*0.75;
    patHelper.world(centerX,centerY);
    var r = (Math.random()-0.5)*40;
    var x = Math.sin(0)*(100+r);
    var y = Math.cos(0)*(100+r);
    ctx.beginPath();
    patHelper.moveTo(x+centerX,y+centerY);
    for(var i = 0 ; i < Math.PI*2; i+= Math.PI/steps){
        r = (Math.random()-0.5)*40;
        var x1 = Math.sin(i+Math.PI/steps/2)*(100+r);
        var y1 = Math.cos(i+Math.PI/steps/2)*(100+r);
        r = (Math.random()-0.5)*40;
        x = Math.sin(i+Math.PI/steps)*(100+r);
        y = Math.cos(i+Math.PI/steps)*(100+r);
        patHelper.quadraticCurveTo(x1+centerX,y1+centerY,x+centerX,y+centerY);
    }
    ctx.fill();
    ctx.stroke();

    //------------------------------------------------------------------------------------------------------
    // test fillText
    ctx.beginPath();
    ctx.font="60px Verdana";
    patHelper.world(canWidth/8,canHeight*0.75);
    patHelper.fillText("Blind as a bat.",canWidth/8,canHeight*0.75);
    
    //------------------------------------------------------------------------------------------------------
    // test strokeText and measureText
    ctx.beginPath();
    ctx.font="60px Verdana";
    patHelper.lineWidth = 8;
    ctx.strokeStyle = pat;
    var textWidth = ctx.measureText("Deaf as a post.").width;
    patHelper.world(canWidth/8+textWidth,canHeight*0.75+60);
    patHelper.strokeText("Deaf as a post.",canWidth/8,canHeight*0.75+60);
    
    //------------------------------------------------------------------------------------------------------
    // all done 
    ctx.setTransform(1,0,0,1,0,0);   // reset the transformation if you need to draw some normal stuff.  
    setTimeout(drawPatterns,20);   // animate just for the fun of it
    ang += 0.01;                 // change the angle

}

// Put PatternHelper code here.
</script>
</body>
</html>




The PatternHelper Object plus instructions in comments.
//------------------------------------------------------------------------------------------------------
// PatternHelper object.
// DIC javascript HTML5 canvas snippet by Blindman67
//
// Helper object handles transformations to provide scalable, oriented, positioned patterns.
// Usage;
//   To create a new helper.
//   patterHelper = new PatterHelper(ctx);  // ctx is canvas 2d context
//                                          // throws an error if context not provided
//   
//  To set the patterns origin
//  patterHelper.origin(x, y);  // x and y is the pixel coordinates that will become the origin of the pattern
//
//  To place the origin on the canvas
//  patternHelper.world(x, y);  // x,y location on the canvas that the patter origin will be.
//  patternHelper.originAt(x, y, worldX, worldY);  // sets pattern and world origins
//  
//  To scale the pattern
//  patterHelper.scale(scale); // the patterns scale 
//                             // will throw error if zero
//
//  To rotate / set orientation
//  patternHelper.rotation(ang, [y]);  // ang is the rotation in radians. Pattern is rotated around its origin plus world space.
//                                     // y is optional. If included the function assumes you are passing a vector pointing in 
//                                     // the direction you wish the pattern to be orientated. The vectors length is the scale 
//                                     // in pixels. rotate(0,2) rotate 90 clockwise and scale 2 times
//
// To reset the transformation in-case you have changed the transformation of the 2d context via transform, setTransform, restore, 
// rotate, scale, or translate 2D context methods use;
// patternHelper.activate();  // no arguments 
//
// Note 1: Because this object uses the transformation the 2d context line width will be affected even if you are not using s pattern 
// for strokes. Set patternHelper.lineWidth to ensure correct line width. Or set patternHelper.lineWidth to undefined (default) if you wish the 
// line width to match the pattern scale, or you want to reset the transform and handle strokes your self.
//
// Note 2: that this object will reset the transformation for the 2D context. If you wish to keep the context settings use save() 
// before calling any of the drawing functions, followed by restore() after drawing.
//
// Note 3: text methods could do with more work. Currently only support px sizes and font must be specified with its pixel size first.
// "60px Arial" is valid while "normal 30px Arial" will not render. Because the font specifier can have many forms I have only
// included limited support. If you desperately need support for a specific format, feel free to contact me.
//
// Text also does not support pattern rotation, only scale and position.
//
// Some browsers do not support maxWidth for fillText and strokeText. If you choose to use this argument you will have to implement 
// your own fallback solution. At this time Safari does not support maxWidth
//
// To draw use PatterHelpers mirror functions. All these functions reset the context's transformation 
// arc, arcTo, rect, fillRect, strokeRect, moveTo, lineTo, quadraticCurveTo, bezierCurveTo, isPointInPath, fillText, strokeText
// They are just the same as the 2d Context functions but transform 
//
// Please note that arcTo is not supported by Opera. This PatternHelper will fail silently and ignore the call.
//
// example
// var ctx = canvasElement.getContext("2d");   // get the 2d context of a canvas element
// var image = new Image();                    // create an image for the pattern
// image.src = "imageURL.type";                // you should wait for the image to load before continuing
// var patternHelper = new PatternHelper(ctx); // create new PatterHelper
// pattern = ctx.createPatter(image);          // create a pattern from the image
// ctx.fillStyle = pattern;                    // set the fill style to the pattern
// patternHelper.origin(image.width/2,image.height/2); // set the patterns origin to its center
// patternHelper.scale(2);                     // double the patterns size
// patternHelper.world(150,150);               // place the patterns origin at canvas (world) coords 150,150
// patternHelper.rotation(Math.PI/2);          // rotate the pattern around its origin 90 deg clockwise
// patternHelper.fillRect(100, 100, 100, 100); // draws a filled rectangle at coordinates 100,100 and with and height 100 
//                                             // containing the pattern
//
//
// Tested on current versions of Chrome Beta, Firefox, and IE on 4th-September-2014
// For more information contact Blindman67 via dreamincode.com in this thread, or via [email protected] Please include "DIC" in
// message subject or you may end up in the email trash.
// Software is provided as is. Every effort has been made to ensure its proper function but you use at your own risk.

function PatternHelper(ctx) {
    if (ctx === undefined) {
        throw("No context provided when creating PatterHelper.");
    }
    // expose line width
    this.lineWidth; // undefined. Set to desired line width and let PatternHelper maintain correct width
                    // or leave undefined and set it your self.
    
    // hidden properties
    var X = 0;
    var Y = 0;
    var offsetX = 0;
    var offsetY = 0;
    var originX = 0;
    var originY = 0;
    var worldX = 0;
    var worldY = 0;
    var scale = 1;
    var invScale = 1;
    var rotation = 0;
    var invMatA;
    var invMatB;
    var ctx = ctx;
    var transformValid = false;
    var textStroke = false;

    //------------------------------------------------------------------------------------------------------
    // hidden function to set the transformation matrix and transform a point back to world space.
    var useHidden = function (x, y) { // x,y point to transform back to world space.
        if (!transformValid) { // update transform if there has been a change
            var matA = Math.cos(-rotation) * (scale); // get vector along X
            var matB = Math.sin(-rotation) * (scale);
            var det = matA * matA - matB * (-matB); // calculate inverse vector
            invMatA = matA / det;
            invMatB = matB / det;
            offsetX = (-originX) * matA + (-originY) * (-matB) + worldX; // get the offset to the patterns absolute origin.
            offsetY = (-originX) * matB + (-originY) * matA + worldY;
            ctx.setTransform(matA, matB, -matB, matA, offsetX, offsetY); // set the transformation
            transformValid = true; // indicate that the transform has been set
        }
        // handle line width if required
        if (this.lineWidth !== undefined) {
            ctx.lineWidth = this.lineWidth * Math.abs(invScale);
        }

        x = x - offsetX; // transform point back to world space.
        y = y - offsetY;
        X = x * invMatA + y * invMatB; // via inverse transformation.
        Y = x * (-invMatB) + y * invMatA;
        // hidden properties X,Y hold the transformed point saving the need to return the coords in an Array or OBject.
        // This ensures that garbage collection does not get called  each time this function is called.
    }
    // bind the above function to this object. This keeps the function hidden but still gives it access to exposed properties and functions
    var use = useHidden.bind(this);
    var stubFunction = function () {}; // empty function for unsupported draw methods


    //------------------------------------------------------------------------------------------------------
    // 2D context mirror functions. Please see canvas 2d context documentation for description
    // Documentation can be found at http://www.w3schools.com/jsref/dom_obj_canvas.asp
    //------------------------------------------------------------------------------------------------------
    this.arc = function (x, y, radius, start, end) {
        use(x, y);
        ctx.arc(X, Y, radius * Math.abs(invScale), start, end);
    }
    //------------------------------------------------------------------------------------------------------
    this.arcTo = function (x, y, x1, y1, radius) {
        use(x, y);
        x = x1 - offsetX;
        y = y1 - offsetY;
        x1 = x * invMatA + y * invMatB;
        y1 = x * (-invMatB) + y * invMatA;
        ctx.arcTo(X, Y, x1, y1, radius * Math.abs(invScale));
    }
    //------------------------------------------------------------------------------------------------------
    if (!ctx.arcTo) { // opera does not support arcTo
        this.arcTo = stubFunction;
        console.log("arcTo unsupported by this browser.");
    }

    //------------------------------------------------------------------------------------------------------
    this.fillText = function (text, x, y, maxWidth) {
        var font = ctx.font;
        var fontSize = font.split("px")[0];
        if (isNaN(fontSize)) {
            return; // can not determine font size. Ignore draw request
        }
        // resize the font 
        fontSize = (Number(fontSize) * Math.abs(invScale)).toFixed(2); // should be precise enough for most situations
        ctx.font = fontSize + "px" + font.split("px")[1];
        var tempRotate = rotation;
        rotation -= rotation; // need to align the text with the world.
        transformValid = false; // as we have modified the transform flag it as invalid.
        use(x, y);
        rotation = tempRotate; // restore rotation
        transformValid = false; // as we have modified the transform flag it as invalid.
        if (maxWidth !== undefined) {
            maxWidth *= Math.abs(invScale);
            if (textStroke) {
                ctx.strokeText(text, X, Y, maxWidth);
            } else {
                ctx.fillText(text, X, Y, maxWidth);
            }
            ctx.font = font; // restore font
            return;
        }
        if (textStroke) {
            ctx.strokeText(text, X, Y);
        } else {
            ctx.fillText(text, X, Y);
        }
        ctx.font = font; // restore font
    }
    //------------------------------------------------------------------------------------------------------
    this.strokeText = function (text, x, y, maxWidth) {
        textStroke = true;
        this.fillText(text, x, y, maxWidth);
        textStroke = false;
    }
    //------------------------------------------------------------------------------------------------------
    this.rect = function (x, y, w, h) {
        use(x, y);
        ctx.moveTo(X, Y);
        ctx.lineTo(X + w * invMatA, Y + w * (-invMatB));
        ctx.lineTo(X + w * invMatA + h * invMatB, Y + w * (-invMatB) + h * invMatA);
        ctx.lineTo(X + h * invMatB, Y + h * invMatA);
        ctx.closePath();
    }
    //------------------------------------------------------------------------------------------------------
    this.fillRect = function (x, y, w, h) {
        this.rect(x, y, w, h);
        ctx.fill();
    }
    //------------------------------------------------------------------------------------------------------
    this.strokeRect = function (x, y, w, h) {
        this.rect(x, y, w, h);
        ctx.stroke();
    }
    //------------------------------------------------------------------------------------------------------
    this.moveTo = function (x, y) {
        use(x, y);
        ctx.moveTo(X, Y);
    }
    //------------------------------------------------------------------------------------------------------
    this.lineTo = function (x, y) {
        use(x, y);
        ctx.lineTo(X, Y);
    }
    //------------------------------------------------------------------------------------------------------
    this.isPointInPath = function (x, y) {
        use(x, y);
        return ctx.isPointInPath(X, Y);
    }
    //------------------------------------------------------------------------------------------------------
    this.quadraticCurveTo = function (x1, y1, x2, y2) {
        use(x1, y1);
        x1 = x2 - offsetX;
        y1 = y2 - offsetY;
        x2 = x1 * invMatA + y1 * invMatB;
        y2 = x1 * (-invMatB) + y1 * invMatA;
        ctx.quadraticCurveTo(X, Y, x2, y2);
    }
    //------------------------------------------------------------------------------------------------------
    this.bezierCurveTo = function (x1, y1, x2, y2, x3, y3) {
        use(x1, y1);
        x1 = x2 - offsetX;
        y1 = y2 - offsetY;
        x2 = x1 * invMatA + y1 * invMatB;
        y2 = x1 * (-invMatB) + y1 * invMatA;
        x1 = x3 - offsetX;
        y1 = y3 - offsetY;
        x3 = x1 * invMatA + y1 * invMatB;
        y3 = x1 * (-invMatB) + y1 * invMatA;
        ctx.bezierCurveTo(X, Y, x2, y2, x3, y3);
    }

    //------------------------------------------------------------------------------------------------------
    // PatternHelper interface
    //------------------------------------------------------------------------------------------------------
    // exposed functions. See comments at the top of this file for usage instructions
    this.activate = function () {
        transformValid = false;
    }
    //------------------------------------------------------------------------------------------------------
    // set canvas position of pattern origin
    this.world = function (x, y) {
        worldX = x;
        worldY = y;
        transformValid = false;
    }
    //------------------------------------------------------------------------------------------------------
    // set pattern origin 0,0 top left of pattern
    this.origin = function (x, y) {
        originX = x;
        originY = y;
        transformValid = false;
    }
    //------------------------------------------------------------------------------------------------------
    // set origin and world in one function
    this.originAt = function (x, y, atX, atY) {
        originX = x;
        originY = y;
        worldX = atX;
        worldY = atY;
        transformValid = false;
    }
    //------------------------------------------------------------------------------------------------------
    // set the pattern scale
    this.scale = function (sc) {
        if (sc === 0) {
            throw("Can not use 0 scale.");
        }
        scale = sc;
        invScale = 1 / scale;
        transformValid = false;
    }
    //------------------------------------------------------------------------------------------------------
    // set rotation. Y optional. See help above for details
    this.rotation = function (rot, y) {
        transformValid = false;
        if (y !== undefined) {
            if (rot === 0 && y === 0) {
                throw("Invalid vector. Length 0.");
            }
            scale = Math.sqrt(rot * rot + y * y);
            invScale = 1 / scale;
            rotation = Math.acos(rot / scale);
            if (y > 0) {
                rotation =  - rotation;
            }
            return;
        }
        rotation = rot;
    }
}
// end of PatternHelper Object
//------------------------------------------------------------------------------------------------------




Is This A Good Question/Topic? 0
  • +

Page 1 of 1