Skip to content

Instantly share code, notes, and snippets.

@rodrigopedra
Last active May 31, 2022 23:51
Show Gist options
  • Save rodrigopedra/fcf8e84ec6dc80f3572b97ae26e2924d to your computer and use it in GitHub Desktop.
Save rodrigopedra/fcf8e84ec6dc80f3572b97ae26e2924d to your computer and use it in GitHub Desktop.
Rotate and scale image around its center using canvas
<html>
<head>
<style>
body {
background-color: #ccc;
}
</style>
</head>
<body>
<canvas id="box" width="500" height="500"></canvas>
<script>
const STEPS = 60; // increase or decrease to adjust speed
const ROTATION_STEP = Math.PI / STEPS;
const SCALE_STEP = Math.pow( 1.5, 1.0 / STEPS ); // 150% at end
const render = function () {
const canvas = document.getElementById( 'box' );
const ctx = canvas.getContext( '2d' );
const image = new Image();
const resetCanvas = function () {
ctx.fillStyle = 'white';
ctx.fillRect( 0, 0, canvas.width, canvas.height );
};
const renderImage = function ( x, y, width, height, angle, scale ) {
const centerX = width / 2.0;
const centerY = height / 2.0;
// save context's current transform state
ctx.save();
// move context's origin to image position
ctx.translate( x, y );
// apply transformations
ctx.rotate( angle );
ctx.scale( scale, scale );
// draw image centered on its position
ctx.drawImage( image, -centerX, -centerY, width, height );
// restore context's previous transform state
ctx.restore();
};
const nextTick = function nextTick ( angle, scale ) {
resetCanvas();
renderImage( 50, 50, 150, 150, angle, scale );
renderImage( 250, 100, 100, 100, angle, scale );
renderImage( 300, 300, 50, 50, angle, scale );
renderImage( 100, 350, 75, 75, angle, scale );
if ( angle < Math.PI * 2.0 ) {
requestAnimationFrame( function () {
const newAngle = angle + ROTATION_STEP;
const newScale = scale * SCALE_STEP;
nextTick( Math.min( Math.PI * 2.0, newAngle ), newScale );
} );
}
};
image.addEventListener( 'load', function () {
requestAnimationFrame( function () { nextTick( 0, 1.0 ); } );
} );
// intialize
resetCanvas();
image.src = 'https://placekitten.com/g/200/200';
};
window.document.addEventListener( 'DOMContentLoaded', render );
</script>
</body>
</html>
<html>
<head>
<style>
body {
background-color: #ccc;
}
</style>
</head>
<body>
<canvas id="box" width="500" height="500"></canvas>
<script>
class Scene
{
constructor ( canvas, backgroundColor ) {
this.canvas = canvas;
this.backgroundColor = backgroundColor;
this.context = canvas.getContext( '2d' );
this.children = [];
}
clear () {
this.context.setTransform( 1, 0, 0, 1, 0, 0 );
this.context.fillStyle = this.backgroundColor;
this.context.fillRect( 0, 0, this.canvas.width, this.canvas.height );
}
addChild ( child ) {
this.children.push( child );
}
nextTick () {
requestAnimationFrame( () => { this.render(); } );
}
render () {
this.clear();
const hasNextTick = this.children.reduce( ( hasNextTick, children ) => {
const childHasNextTick = children.render( this.context );
return hasNextTick || (childHasNextTick === true);
}, false );
if ( hasNextTick ) {
requestAnimationFrame( () => { this.nextTick(); } );
}
}
}
class SceneChild
{
constructor ( x = 0, y = 0, scale = 1.0, angle = 0.0 ) {
this.x = x;
this.y = y;
this.scale = scale;
this.angle = angle;
this.originX = 0;
this.originY = 0;
}
moveOrigin ( x, y ) {
this.originX = x;
this.originY = y;
}
render ( context ) {
// save previous transform state
context.save();
// move context's origin to child position
context.translate( this.x, this.y );
// apply transforms
context.scale( this.scale, this.scale );
context.rotate( this.angle );
// ABSTRACT : draw child centered on its origin
this.drawChild( context, -this.originX, -this.originY );
// restore transform state
context.restore();
}
}
class ImageChild extends SceneChild
{
constructor ( image, x = 0, y = 0, scale = 1.0, angle = 0.0 ) {
super( x, y, scale, angle );
this.image = image;
this.width = image.width;
this.height = image.height;
}
drawChild ( context, x, y ) {
// draw image with its center on the origin
context.drawImage( this.image, x, y, this.width, this.height );
}
}
class TextChild extends SceneChild
{
constructor ( text, size, context, x = 0, y = 0, scale = 1.0, angle = 0.0 ) {
super( x, y, scale, angle );
this.text = text;
this.size = size;
context.font = size + 'px sans-serif';
this.width = context.measureText( text ).width;
this.height = size;
}
drawChild ( context, x, y ) {
context.font = this.size + 'px sans-serif';
context.textBaseline = 'top';
context.fillStyle = 'blue';
context.fillText( this.text, x, y );
}
}
class Tween
{
constructor ( steps, rotateTo, scaleTo ) {
this.steps = steps;
this.rotateTo = rotateTo;
this.scaleTo = scaleTo;
this.angleIncrement = rotateTo / steps;
this.scaleIncrement = Math.pow( scaleTo, 1.0 / steps );
this.children = [];
}
addChild ( child ) {
this.children.push( child );
}
render ( context ) {
this.children.forEach( ( child ) => {
if ( this.steps > 0 ) {
child.angle = Math.min( this.rotateTo, child.angle + this.angleIncrement );
child.scale = Math.min( this.scaleTo, child.scale * this.scaleIncrement );
}
child.render( context );
} );
this.steps--;
return this.steps > 0;
}
}
const initialize = function () {
const scene = new Scene( document.getElementById( 'box' ), 'white' );
const image = new Image();
scene.nextTick();
image.addEventListener( 'load', function () {
const imagesTween = new Tween( 60, Math.PI * 2, 1.5 );
let imageChild = new ImageChild( image, 100, 100, 0.5 );
// move origin to image's center
imageChild.moveOrigin( imageChild.width / 2.0, imageChild.height / 2.0 );
imagesTween.addChild( imageChild );
imageChild = new ImageChild( image, 370, 50, 0.5 );
imageChild.moveOrigin( imageChild.width / 2.0, imageChild.height / 2.0 );
imagesTween.addChild( imageChild );
imageChild = new ImageChild( image, 120, 280, 1.2 );
imageChild.moveOrigin( imageChild.width / 2.0, imageChild.height / 2.0 );
imagesTween.addChild( imageChild );
imageChild = new ImageChild( image, 350, 350, 0.75 );
imageChild.moveOrigin( imageChild.width / 2.0, imageChild.height / 2.0 );
imagesTween.addChild( imageChild );
scene.addChild( imagesTween );
const textTween = new Tween( 30, Math.PI * 2, 5 );
const textChild = new TextChild( 'Cats!', 30, scene.context, 250, 250 );
textChild.moveOrigin( textChild.width / 2.0, textChild.height / 2.0 );
textTween.addChild( textChild );
scene.addChild( textTween );
scene.nextTick();
} );
image.src = 'https://placekitten.com/g/200/200';
};
window.document.addEventListener( 'DOMContentLoaded', initialize );
</script>
</body>
</html>
@rodrigopedra
Copy link
Author

Revisited and fixed calculations

@rodrigopedra
Copy link
Author

Use context and image from the outer scope

@rodrigopedra
Copy link
Author

Modified scale increment calculation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment