yenren
yenren

Reputation: 501

On canvas, scale shape from its center

I have a circle at [x, y] with a radius r. I want to scale it by x1.1, while keeping it at the same [x, y] position. I tried suggested code from other questions but I just couldn't do it.

Here is my code:

ctx.save();
ctx.translate(x, y);
ctx.scale(1.1, 1.1);

ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fillStyle = '#000000';
ctx.fill();

ctx.restore();

Any suggestions? Thanks.

Upvotes: 0

Views: 980

Answers (2)

Blindman67
Blindman67

Reputation: 54026

Coordinate systems

When rendering complex scenes containing many elements it is common to divide the scene into several abstract coordinate systems.

This allows you to easily translate, scale, rotate elements within the scene via a transforms.

A transform defines an element's, x and y position (translation) the direction of the x and y axis (rotation and or skew), and how far apart each pixel's edge is along these axis (scale)

3 common coordinate systems

  • World: The absolute coordinates of each element. This is the top most coordinate, with elements within (child elements) having coordinates relative to themselves.

  • Local: The relative coordinates of an element's parts (path, text, etc). For example a rectangles local coordinate is its center {x: 0, y: 0} its top left corner is half the with and height up and left of the origin {x: -width / w, y: -height / 2}

    Local coordinates can also be joined as a tree, where the parent local becomes the world coordinates for the children.

  • View: (AKA screen, canvas) This is the coordinate system of the display and is used to manipulate the scale, position, orientation of all items in the world. It is most often independent of the world and local coordinates systems, however a common shortcut is to make the world coordinates match the view (as done in rest of answer).

Using coordinate systems

You can use these abstraction coordinate systems in part or full to make your scene building code easier to understand and manipulate.

In the following snippet we can define an element (a circle), relative to it's local coordinates, and render it via it's world coordinates which can match the view coordinates.

Thus the circle need only store its radius, to draw it you provide the world/view coordinate and scale.

const Circle = {
    radius: 100, x: 0, y: 0,    // Local coords at circle center
    draw(wx, wy, wScale) {      // World / View coords
        ctx.setTransform(wScale, 0, 0, wScale, wx, wy);
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
        ctx.stroke();
    }
}
Circle.draw(100, 50, 1.2);  // draw at 100, 50, scale by 1.2

// Note to reset the default transform use ctx.setTransform(1,0,0,1,0,0);

In the above code if you scale the circle up by 2 it will also double the line width as line width is part of the circle's local coordinates. To change the size of the circle without changing the line width you change its radius.

Example

A more complex example adds rotations and some extra element types and draws these shapes are randomly position scaled and rotated on the canvas.

Note how the line width is scaled with the shape's scale. The text is however is a fill and there is no stroke to scale.

const ctx = can.getContext("2d");
// When code ready draw scene
requestAnimationFrame(() => drawRandomShapes(shapes, 40));

const ShapeCommon = {
    draw(x, y, scale, ang) {  // ang in radians
        ctx.setTransform(scale, 0, 0, scale, x, y);
        ctx.rotate(ang);
        ctx.beginPath();
        if (this.addPath() !== false) { ctx.stroke() }
    }
};
const Circle = (r = 100, s = 0, e = Math.PI * 2) => ({
    ...ShapeCommon,
    addPath() { ctx.arc(0, 0, r, s, e); }
});
const Rectangle = (w, h = w) => ({
    ...ShapeCommon,
    addPath() { ctx.rect(- w * 0.5, -h  * 0.5, w, h); }
});
const Text = (str) => ({
    ...ShapeCommon,
    addPath() { 
        ctx.textAlign = "center";
        ctx.fillText(str, 0, 0); 
        return false; // true to indicate content has been rendered
    }
});
// Example creates some shapes to draw
const shapes = [Circle(20, 0, Math.PI), Circle(5), Rectangle(20), Rectangle(20, 3), Text("Hi local")];
function drawRandomShapes(shapes, count) {
    const w = can.width;
    const h = can.height;
    while (count--) {
         const shapeIdx = Math.random() * shapes.length | 0;
         const x = Math.random() * w;
         const y = Math.random() * h;
         const scale = Math.random() * 4 + 0.2;
         const rot = Math.random() * Math.PI * 2;
         shapes[shapeIdx].draw(x, y, scale, rot);
    }
}         
canvas { border: 1px solid black; }
<canvas id="can" width="512" height="512"></canvas>

Upvotes: 2

P&#225;draig Galvin
P&#225;draig Galvin

Reputation: 1155

ctx.scale will change the scale of the entire canvas. Using a scale of 1.1 will make each unit of the canvas use 1.1 pixels. You need to take that into account in your transformations if you want to use pixel measurements for positioning.

If you only intend to scale the circle by 1.1, you can just multiply the radius to increase its size:

ctx.save();
ctx.translate(x, y);

ctx.beginPath();
ctx.arc(x, y, r * 1.1, 0, 2 * Math.PI);
ctx.fillStyle = '#000000';
ctx.fill();

ctx.restore();

Upvotes: 1

Related Questions