Last active
May 31, 2022 23:51
-
-
Save rodrigopedra/fcf8e84ec6dc80f3572b97ae26e2924d to your computer and use it in GitHub Desktop.
Rotate and scale image around its center using canvas
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
Remove useless resetCanvas
before first renderImage
call
Revisited and fixed calculations
Use context and image from the outer scope
Modified scale increment calculation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated to not use
setTimeout