Canvas: How to modify ImageData pixels that exist within a custom path? - javascript

I'm trying to create an experience where a user can move a custom cursor over an image and alter the pixels that live within that custom cursor.
In the mousemove event, I try to recreate this custom cursor path and use isPointInPath to determine if the custom cursor is "over" a given point in the main canvas's context.
The code does alter some pixels in the main canvas. However, the following unexpected behaviors are listed below:
moving the cursor alters the pixels of more than one area in the main canvas
the x and y coordinates of the main canvas (derived from imageData) are not correct
(See the image below)
Why is this happening? How could the code be changed such that the custom cursor (aka circle) only changes pixels corresponding to that part of the main canvas?
let image;
let imageData;
const cursorCanvas = document.createElement("canvas");
const cursorCanvasContext = cursorCanvas.getContext("2d");
cursorCanvas.width = 100;
cursorCanvas.height = 100;
drawCursor();
const imageCanvas = document.querySelector("canvas");
imageCanvas.width = window.innerWidth;
imageCanvas.height = window.innerHeight;
const imageCanvasContext = imageCanvas.getContext("2d");
document.querySelector("img").addEventListener("load", (event) => {
image = event.target;
image.crossOrigin = "Anonymous";
drawBackground();
imageData = imageCanvasContext.getImageData(
0,
0,
imageCanvas.width,
imageCanvas.height
);
});
window.addEventListener("mousemove", (event) => {
requestAnimationFrame(() => {
if (imageData) {
imageCanvasContext.beginPath();
imageCanvasContext.arc(
event.clientX,
event.clientY,
cursorCanvas.width / 2,
0,
2 * Math.PI
);
imageCanvasContext.closePath();
for (let i = 0; i < imageData.data.length; i += 4) {
const y = Math.floor(i / imageData.width);
const x = i - y * imageData.width;
if (imageCanvasContext.isPointInPath(x, y)) {
imageData.data[i] = 40;
imageData.data[i + 1] = 40;
imageData.data[i + 2] = 40;
}
}
imageCanvasContext.putImageData(imageData, 0, 0);
}
imageCanvasContext.drawImage(
cursorCanvas,
event.clientX - cursorCanvas.width / 2,
event.clientY - cursorCanvas.height / 2
);
});
});
function drawBackground() {
imageCanvasContext.clearRect(
0,
0,
imageCanvas.width,
imageCanvas.height
);
if (image) {
const ratio =
window.innerWidth > window.innerHeight ?
window.innerWidth / window.innerHeight :
window.innerHeight / window.innerWidth;
imageCanvasContext.drawImage(
image,
0,
0,
image.width,
image.height,
0,
0,
ratio * imageCanvas.width,
ratio * imageCanvas.height
);
}
}
function drawCursor() {
cursorCanvasContext.beginPath();
cursorCanvasContext.arc(
cursorCanvas.width / 2,
cursorCanvas.width / 2,
cursorCanvas.width / 2,
0,
2 * Math.PI
);
cursorCanvasContext.stroke();
cursorCanvasContext.closePath();
}
html,
body {
width: 100vw;
height: 100vh;
overflow: hidden;
}
body {
font-family: sans-serif;
margin: 0;
}
img {
visibility: hidden;
position: absolute;
}
canvas {
cursor: none;
}
<img src="https://i.imgur.com/vzGhITt.jpeg" />
<canvas />

Ah, it seems that I have miscalculated the x and y coordinates of the imageData.
Incorrect:
const y = Math.floor(i / imageData.width);
const x = i - y * imageData.width;
Correct:
const x = (i / 4) % imageData.width;
const y = Math.floor(i / 4 / imageData.width);
let image;
let imageData;
const cursorCanvas = document.createElement("canvas");
const cursorCanvasContext = cursorCanvas.getContext("2d");
cursorCanvas.width = 100;
cursorCanvas.height = 100;
drawCursor();
const imageCanvas = document.querySelector("canvas");
imageCanvas.width = window.innerWidth;
imageCanvas.height = window.innerHeight;
const imageCanvasContext = imageCanvas.getContext("2d");
document.querySelector("img").addEventListener("load", (event) => {
image = event.target;
image.crossOrigin = "Anonymous";
drawBackground();
imageData = imageCanvasContext.getImageData(
0,
0,
imageCanvas.width,
imageCanvas.height
);
});
window.addEventListener("mousemove", (event) => {
if (imageData) {
imageCanvasContext.beginPath();
imageCanvasContext.arc(
event.clientX,
event.clientY,
cursorCanvas.width / 2,
0,
2 * Math.PI
);
imageCanvasContext.closePath();
for (let i = 0; i < imageData.data.length; i += 4) {
const x = (i / 4) % imageData.width;
const y = Math.floor(i / 4 / imageData.width);
if (imageCanvasContext.isPointInPath(x, y)) {
imageData.data[i] = 40;
imageData.data[i + 1] = 40;
imageData.data[i + 2] = 40;
}
}
imageCanvasContext.putImageData(imageData, 0, 0);
}
imageCanvasContext.drawImage(
cursorCanvas,
event.clientX - cursorCanvas.width / 2,
event.clientY - cursorCanvas.height / 2
);
});
function drawBackground() {
imageCanvasContext.clearRect(
0,
0,
imageCanvas.width,
imageCanvas.height
);
if (image) {
const ratio =
window.innerWidth > window.innerHeight ?
window.innerWidth / window.innerHeight :
window.innerHeight / window.innerWidth;
imageCanvasContext.drawImage(
image,
0,
0,
image.width,
image.height,
0,
0,
ratio * imageCanvas.width,
ratio * imageCanvas.height
);
}
}
function drawCursor() {
cursorCanvasContext.beginPath();
cursorCanvasContext.arc(
cursorCanvas.width / 2,
cursorCanvas.width / 2,
cursorCanvas.width / 2,
0,
2 * Math.PI
);
cursorCanvasContext.stroke();
cursorCanvasContext.closePath();
}
html,
body {
width: 100vw;
height: 100vh;
overflow: hidden;
}
body {
font-family: sans-serif;
margin: 0;
}
img {
visibility: hidden;
position: absolute;
}
canvas {
cursor: none;
}
<img src="https://i.imgur.com/vzGhITt.jpeg" />
<canvas />

Related

Enable to scale a ImageData from a CanvasRenderingContext2D into a other one

Here I have my main canvas 2D context that is on my screen named baseCtx that is in the custom service drawingService.
zoomCtx is the CanvasRenderingContext2D of my second canvas and zoomCanvas is the HTMLCanvasElement of my secondary canvas.
I want to take a rectangle that is the size of a smaller canvas that have for center my mouse.
CurrentX and CurrentY are the current mousePosition on my main canvas.
That is working just fine I can see the content of the rectangle zone on the secondary canvas.
The problem come when I try to zoom in the secondary canvas that will play the role of a magnifying glass. I can see the zone around my cursor drawing on the secondary canvas, it is just not zoom in.
I just won't zoom event tho I am using the scale method.
Here is my code that is called each time the mouse move
calculateZoomedPixel() {
let sx = this.currentX - this.zoomCanvas.width / 2;
let sy = this.currentY - this.zoomCanvas.height / 2;
const zoom = 2;
let image = this.drawingService.baseCtx.getImageData(sx, sy, this.zoomCanvas.width, this.zoomCanvas.height);
this.zoomCtx.putImageData(image, 0, 0);
this.zoomCtx.translate(zoom * this.zoomCanvas.width, zoom * this.zoomCanvas.height);
this.zoomCtx.scale(zoom, zoom);
this.zoomCtx.drawImage(this.zoomCanvas, 0, 0);
this.zoomCtx.translate(-zoom * this.zoomCanvas.width, -zoom * this.zoomCanvas.height);
}
Here is an example of what i want the second canvas to look like:
And here is an example of my application (The smaller canvas to the left should be zoom in):
A few problems here:
Your translate calls are not made properly, you are only translating back after you drew the image, which has no good effect.
You never reset the transformation matrix after you set its scale, so at second call, your zoom canvas as its scale set to 4, at third, to 8 etc.
To workaround both these problems, you can use the absolute setTransform() method to set both the scale and the translate in one call, and then translate back using the drawImage's parameters:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
mag_ctx.globalCompositeOperation = "copy";
source.onmousemove = (evt) => {
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
const image = source_ctx.getImageData(sx, sy, mag_width, mag_height);
mag_ctx.putImageData(image, 0, 0);
// set the zoom and translate to center in one call
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// translate back in drawImage to center our image
mag_ctx.drawImage(magnifier, -mag_width/2, -mag_height/2);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>
But you should not call getImageData in a mousemove event like this. getImageData is utterly slow as it requires to move the context's buffer from the GPU to the CPU. Call it only once to grab the full canvas and reuse only that single ImageData:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
// get the image data only once, when the drawing is done
const image = source_ctx.getImageData(0, 0, source_width, source_height );
source.onmousemove = (evt) => {
mag_ctx.clearRect(0, 0, mag_width, mag_height);
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
// draw the correct portion of the ImageData
mag_ctx.putImageData(image, -sx, -sy, sx, sy, source_width, source_height);
// set the zoom and translate to center in one call
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// translate back in drawImage to center our image
mag_ctx.drawImage(magnifier, -mag_width/2, -mag_height/2);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>
And if you are not doing anything with these pixels' data, then just use drawImage directly:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
source.onmousemove = (evt) => {
mag_ctx.clearRect(0, 0, mag_width, mag_height);
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// draw source canvas directly
mag_ctx.drawImage(source, sx, sy, mag_width, mag_height, -mag_width/2, -mag_height/2, mag_width, mag_height);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>

HTML h1 tag not displaying correctly above javascript+css animation

I have the following code (from a code pen fork), and wish to add a simple Html H1 tag above the animation on the page. I have tried the below, but for some reason the H1 tag just displays for a second (flashes) and then disappears off the screen.
I have tried placing it in several places and also used tags, but the same thing happens.
Can someone please suggest a fix and also explain why this is occuring?
html, javascript file: and CSS File (style.css)
class Canvas {
constructor(element, ctx, w, h) {
this.element = element
this.ctx = ctx
this.width = w
this.height = h
this.interactive = false
this.playing = true
this.point = {
value: 150,
speed: 0.25,
limit: 70,
floor: 10,
up: true,
animating: false
}
this.multiplier = {
value: 1,
speed: 0.005,
limit: 20,
floor: 1,
up: true,
animating: true
}
this.center = {
x: w / 2,
y: h / 2,
targetX: w / 2,
targetY: h / 2,
easing: 0.02
}
this.radius = {
val: h / 2.2,
targetVal: h / 2.2,
easing: 0.02
}
document.body.addEventListener('click', this.click.bind(this))
document.body.addEventListener('mousemove', this.move.bind(this))
document.body.addEventListener('keyup', this.keyup.bind(this))
this.hue = 160
}
click(e) {
this.interactive = !this.interactive
if (!this.interactive) {
this.center.targetX = this.width / 2
this.center.targetY = this.height / 2
this.radius.targetVal = this.height / 2.2
this.element.classList.remove('interactive')
} else {
this.element.classList.add('interactive')
}
}
move(e) {
if (!this.interactive) {
return
}
const h3 = this.height / 3
this.center.targetX = e.pageX
this.center.targetY = Math.max(e.pageY, h3)
this.radius.targetVal = h3 + (e.pageY * 0.8)
}
keyup(e) {
if (e.which != 32) {
return
}
this.playing = !this.playing
if (this.playing && this.drawLoop) {
this.drawLoop()
}
}
update() {
this.clear()
this.animate(this.point)
this.animate(this.multiplier)
this.ease(this.center)
this.ease(this.radius)
this.hue += 0.3
const h = (this.hue % 360)
this.ctx.fillStyle = 'hsl(' + h + ',70%,20%)'
this.ctx.strokeStyle = 'hsla(' + h + ',80%,60%,0.2)'
this.ctx.globalCompositeOperation = 'lighter'
}
clear() {
this.ctx.globalCompositeOperation = 'source-over'
this.ctx.fillStyle = 'rgba(0,0,0,0.1)'
this.ctx.rect(0, 0, this.width, this.height)
this.ctx.fill()
}
draw() {
let radius = this.radius.val
const w2 = this.center.x,
h2 = this.center.y
this.ctx.beginPath()
this.ctx.shadowBlur = 0
this.ctx.shadowColor = 'black'
const points = this.point.value
const multiplier = this.multiplier.value
for (let p = 0; p < points; p++) {
const t = (p / points) * Math.PI * 2
const t2 = ((p * multiplier) / points) * Math.PI * 2
const x = radius * Math.cos(t) + w2
const y = radius * Math.sin(t) + h2
const x2 = radius * Math.cos(t2) + w2
const y2 = radius * Math.sin(t2) + h2
this.ctx.moveTo(x, y)
this.ctx.lineTo(x2, y2)
}
this.ctx.arc(w2, h2, radius, 0, 2 * Math.PI)
this.ctx.stroke()
this.ctx.closePath()
}
animate(object) {
if (!object.animating) {
return
}
if (object.up) {
object.value += object.speed
} else {
object.value -= object.speed
}
if (object.value > object.limit) {
object.up = false
} else if (object.value < object.floor) {
object.up = true
}
}
ease(object) {
if (object.val) {
const dv = object.targetVal - object.val
object.val += dv * object.easing
return
}
const dx = object.targetX - object.x
const dy = object.targetY - object.y
object.x += dx * object.easing
object.y += dy * object.easing
}
random(from, to) {
return from + (Math.rand() * (to - from))
}
resize(w, h) {
this.width = w
this.height = h
this.center.targetX = w / 2
this.center.targetY = h / 2
this.radius.targetVal = h / 2.2
}
}
(_ => {
const canvasElement = document.getElementById('canvas'),
ctx = canvasElement.getContext('2d')
let w = canvasElement.width = window.innerWidth,
h = canvasElement.height = window.innerHeight,
density = 1
const canvas = new Canvas(canvasElement, ctx, w, h)
function setup() {
window.addEventListener('resize', resize)
density = window.devicePixelRatio != undefined ? window.devicePixelRatio : 1.0
canvasElement.width = w * density
canvasElement.height = h * density
canvas.width = w
canvas.height = h
canvas.drawLoop = draw
ctx.scale(density, density)
draw()
}
function draw() {
canvas.update()
canvas.draw()
if (canvas.playing) {
window.requestAnimationFrame(draw)
}
}
function resize() {
w = canvasElement.width = window.innerWidth
h = canvasElement.height = window.innerHeight
canvasElement.width = w * density
canvasElement.height = h * density
canvas.resize(w, h)
ctx.scale(density, density)
}
setup()
})()
body {
background: #000;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
canvas.interactive {
cursor: none;
}
<html>
<head>
<link rel="stylesheet" href="style.css">
<center>
<h1>
<font color="white">This is the H1 Tag</font>
</h1>
</center>
</head>
<body>
<br>
<br>
<br>
<br>
<canvas id="canvas"></canvas>
Update
Thanks to the answer below, I have changed the css to the following, and the tag does display, but the animation is not centered. How can the canvas be re-positioned so that other 'elements' or objects can be fit around it?
canvas {
width: 30%;
height: 30%;
position: absolute;
left: 0;
top: 0;
}
This is because your canvas style is set to take up the whole page. Your h1 is also inside the head of the document. So that needs moved into the body too.
canvas {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
If you remove the canvas style, your h1 will sit above the canvas.
Edit:
Reading between the lines OP wants the image scaled down and centered. I've created a Fiddle to show how this can be achieved.
Fiddle
Add some z-index to your h1 and remove the obsolete tags:
class Canvas {
constructor(element, ctx, w, h) {
this.element = element
this.ctx = ctx
this.width = w
this.height = h
this.interactive = false
this.playing = true
this.point = {
value: 150,
speed: 0.25,
limit: 70,
floor: 10,
up: true,
animating: false
}
this.multiplier = {
value: 1,
speed: 0.005,
limit: 20,
floor: 1,
up: true,
animating: true
}
this.center = {
x: w / 2,
y: h / 2,
targetX: w / 2,
targetY: h / 2,
easing: 0.02
}
this.radius = {
val: h / 2.2,
targetVal: h / 2.2,
easing: 0.02
}
document.body.addEventListener('click', this.click.bind(this))
document.body.addEventListener('mousemove', this.move.bind(this))
document.body.addEventListener('keyup', this.keyup.bind(this))
this.hue = 160
}
click(e) {
this.interactive = !this.interactive
if (!this.interactive) {
this.center.targetX = this.width / 2
this.center.targetY = this.height / 2
this.radius.targetVal = this.height / 2.2
this.element.classList.remove('interactive')
} else {
this.element.classList.add('interactive')
}
}
move(e) {
if (!this.interactive) {
return
}
const h3 = this.height / 3
this.center.targetX = e.pageX
this.center.targetY = Math.max(e.pageY, h3)
this.radius.targetVal = h3 + (e.pageY * 0.8)
}
keyup(e) {
if (e.which != 32) {
return
}
this.playing = !this.playing
if (this.playing && this.drawLoop) {
this.drawLoop()
}
}
update() {
this.clear()
this.animate(this.point)
this.animate(this.multiplier)
this.ease(this.center)
this.ease(this.radius)
this.hue += 0.3
const h = (this.hue % 360)
this.ctx.fillStyle = 'hsl(' + h + ',70%,20%)'
this.ctx.strokeStyle = 'hsla(' + h + ',80%,60%,0.2)'
this.ctx.globalCompositeOperation = 'lighter'
}
clear() {
this.ctx.globalCompositeOperation = 'source-over'
this.ctx.fillStyle = 'rgba(0,0,0,0.1)'
this.ctx.rect(0, 0, this.width, this.height)
this.ctx.fill()
}
draw() {
let radius = this.radius.val
const w2 = this.center.x,
h2 = this.center.y
this.ctx.beginPath()
this.ctx.shadowBlur = 0
this.ctx.shadowColor = 'black'
const points = this.point.value
const multiplier = this.multiplier.value
for (let p = 0; p < points; p++) {
const t = (p / points) * Math.PI * 2
const t2 = ((p * multiplier) / points) * Math.PI * 2
const x = radius * Math.cos(t) + w2
const y = radius * Math.sin(t) + h2
const x2 = radius * Math.cos(t2) + w2
const y2 = radius * Math.sin(t2) + h2
this.ctx.moveTo(x, y)
this.ctx.lineTo(x2, y2)
}
this.ctx.arc(w2, h2, radius, 0, 2 * Math.PI)
this.ctx.stroke()
this.ctx.closePath()
}
animate(object) {
if (!object.animating) {
return
}
if (object.up) {
object.value += object.speed
} else {
object.value -= object.speed
}
if (object.value > object.limit) {
object.up = false
} else if (object.value < object.floor) {
object.up = true
}
}
ease(object) {
if (object.val) {
const dv = object.targetVal - object.val
object.val += dv * object.easing
return
}
const dx = object.targetX - object.x
const dy = object.targetY - object.y
object.x += dx * object.easing
object.y += dy * object.easing
}
random(from, to) {
return from + (Math.rand() * (to - from))
}
resize(w, h) {
this.width = w
this.height = h
this.center.targetX = w / 2
this.center.targetY = h / 2
this.radius.targetVal = h / 2.2
}
}
(_ => {
const canvasElement = document.getElementById('canvas'),
ctx = canvasElement.getContext('2d')
let w = canvasElement.width = window.innerWidth,
h = canvasElement.height = window.innerHeight,
density = 1
const canvas = new Canvas(canvasElement, ctx, w, h)
function setup() {
window.addEventListener('resize', resize)
density = window.devicePixelRatio != undefined ? window.devicePixelRatio : 1.0
canvasElement.width = w * density
canvasElement.height = h * density
canvas.width = w
canvas.height = h
canvas.drawLoop = draw
ctx.scale(density, density)
draw()
}
function draw() {
canvas.update()
canvas.draw()
if (canvas.playing) {
window.requestAnimationFrame(draw)
}
}
function resize() {
w = canvasElement.width = window.innerWidth
h = canvasElement.height = window.innerHeight
canvasElement.width = w * density
canvasElement.height = h * density
canvas.resize(w, h)
ctx.scale(density, density)
}
setup()
})()
body {
background: #000;
overflow: hidden;
}
h1 {
position:relative;
z-index:2;
color:#fff;
text-align:center;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
canvas.interactive {
cursor: none;
}
<canvas id="canvas"></canvas>
<h1>This is a title</h1>
This is because of your canvas styles. If you add a margin-top to your canvas the h1 will not be removed, try it like this:
canvas {
margin-top: 200px;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
You'll see that now your h1 is not dissapearing anymore.
Edit
Reading between the lines OP appears to want it scaled down and centered, so I've created a fiddle with updated code.
Fiddle

Canvas arc on Retina is too sharp at some points

I am trying to create animated arc that doesn't look blurry on HiDPI devices.
This is how my arc looks on iPhone 5s:
you can see that near 0, 90, 180 deg arc becomes too sharp. How can I prevent this?
This is my code:
// Canvas arc progress
const can = document.getElementById('canvas');
const ctx = can.getContext('2d');
const circ = Math.PI * 2;
const quart = Math.PI / 2;
const canvasSize = can.offsetWidth;
const halfCanvasSize = canvasSize / 2;
let start = 0,
finish = 70,
animRequestId = null;
// Get pixel ratio
const ratio = (function() {
const dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
})();
// Set canvas h & w
can.width = can.height = canvasSize * ratio;
can.style.width = can.style.height = canvasSize + 'px';
ctx.scale(ratio, ratio)
ctx.beginPath();
ctx.strokeStyle = 'rgb(120,159,194)';
ctx.lineCap = 'square';
ctx.lineWidth = 8.0;
ctx.arc(halfCanvasSize, halfCanvasSize, halfCanvasSize - 4, 0, circ, false);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.strokeStyle = 'rgb(244,247,255)';
ctx.lineCap = 'round';
ctx.lineWidth = 8.0;
ctx.closePath();
let imd = ctx.getImageData(0, 0, canvasSize, canvasSize);
const draw = (current) => {
ctx.putImageData(imd, 0, 0);
ctx.beginPath();
ctx.arc(halfCanvasSize, halfCanvasSize, halfCanvasSize - 4, -(quart), ((circ) * current) - quart, false);
ctx.stroke();
};
(function animateArcProgress() {
animRequestId = requestAnimationFrame(animateArcProgress);
if (start <= finish) {
draw(start / 100);
start += 2;
} else {
cancelAnimationFrame(animRequestId);
}
})();
body {
margin: 0;
}
div {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
widht: 300px;
background: #85b1d7;
}
canvas {
height: 250px;
width: 250px;
}
<div>
<canvas id='canvas'></canvas>
</div>
You can soften the edge by drawing 1/2 pixel inside as done in your code below.
I rendered the arc 3 times at 8, 7.5 and 7 pixel width width alpha colour values 0.25, 0.5 and 1 respectively.
You can make it as soft as you want.
BTW using putImageData is very slow, why not just render the background to another canvas and draw that via ctx.drawImage(offScreencanvas,0,0) that way you will use the GPU to render the background rather than the CPU via the graphics port bus.
I added a bit more code to show the different softening FX you can get and added a mouse zoom so you can see the pixels a little better.
const can = document.getElementById('canvas');
const can2 = document.createElement("canvas"); // off screen canvas
const can3 = document.createElement("canvas"); // off screen canvas
const ctx = can.getContext('2d');
const ctx2 = can2.getContext('2d');
const ctx3 = can3.getContext('2d');
const circ = Math.PI * 2;
const quart = Math.PI / 2;
const canvasSize = can.offsetWidth;
const halfCanvasSize = canvasSize / 2;
const mouse = {x : null, y : null};
can.addEventListener("mousemove",function(e){
var bounds = can.getBoundingClientRect();
mouse.x = e.clientX - bounds.left;
mouse.y = e.clientY - bounds.top;
});
let start = 0,
finish = 70,
animRequestId = null;
// Get pixel ratio
const ratio = (function() {
const dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
})();
// Set canvas h & w
can2.height = can3.height = can2.width = can3.width = can.width = can.height = canvasSize * ratio;
can.style.width = can.style.height = canvasSize + 'px';
ctx.scale(ratio, ratio)
ctx2.scale(ratio, ratio)
ctx3.scale(ratio, ratio)
ctx2.beginPath();
ctx2.strokeStyle = 'rgb(120,159,194)';
ctx2.lineCap = 'square';
ctx2.lineWidth = 8.0;
ctx2.arc(halfCanvasSize, halfCanvasSize, halfCanvasSize - 4, 0, circ, false);
ctx2.stroke();
ctx2.closePath();
ctx2.beginPath();
ctx2.strokeStyle = 'rgb(244,247,255)';
ctx2.lineCap = 'round';
ctx2.lineWidth = 8.0;
ctx2.closePath();
const draw = (current) => {
ctx3.clearRect(0,0,canvas.width,canvas.height);
ctx3.drawImage(can2,0,0);
var rad = halfCanvasSize - 4;
const drawArc = () => {
ctx3.beginPath();
ctx3.arc(halfCanvasSize, halfCanvasSize, rad, -(quart), ((circ) * current) - quart, false);
ctx3.stroke();
}
// draw soft
ctx3.strokeStyle = 'rgb(244,247,255)';
ctx3.lineWidth = 8.5;
ctx3.globalAlpha = 0.25;
drawArc();;
ctx3.lineWidth = 7.0;
ctx3.globalAlpha = 0.5;
drawArc();;
ctx3.lineWidth = 6.5;
ctx3.globalAlpha = 1;
drawArc();
// draw normal
rad -= 12;
ctx3.lineWidth = 8.0;
ctx3.globalAlpha = 1;
drawArc();;
// draw ultra soft
rad -= 12;
ctx3.strokeStyle = 'rgb(244,247,255)';
ctx3.lineWidth = 9.0;
ctx3.globalAlpha = 0.1;
drawArc();
ctx3.lineWidth = 8.0;
ctx3.globalAlpha = 0.2;
drawArc();;
ctx3.lineWidth = 7.5;
ctx3.globalAlpha = 0.5;
drawArc();
ctx3.lineWidth = 6;
ctx3.globalAlpha = 1;
drawArc();
};
const zoomW = 30;
const zoomAmount = 5;
const drawZoom = () => {
ctx.drawImage(can3,0,0);
var width = zoomW * zoomAmount;
var cx = mouse.x - width / 2;
var cy = mouse.y - width / 2;
var c1x = mouse.x - zoomW / 2;
var c1y = mouse.y - zoomW / 2;
ctx.strokeStyle = 'rgb(244,247,255)';
ctx.lineWidth = 4;
ctx.strokeRect(cx,cy,width,width);
ctx.clearRect(cx,cy,width,width);
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.drawImage(can3,c1x,c1y,zoomW,zoomW,cx,cy,width,width);
ctx.imageSmoothingEnabled = true;
ctx.mozImageSmoothingEnabled = true;
}
function keepUpdating(){
ctx.clearRect(0,0,can.width,can.height);
drawZoom();
requestAnimationFrame(keepUpdating);
}
(function animateArcProgress() {
ctx.clearRect(0,0,can.width,can.height);
draw(start / 100);
drawZoom();
if (start <= finish) {
start += 0.5;
requestAnimationFrame(animateArcProgress);
} else {
requestAnimationFrame(keepUpdating);
}
})();
body {
margin: 0;
}
div {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
widht: 300px;
background: #85b1d7;
}
canvas {
height: 250px;
width: 250px;
}
<div>
<canvas id='canvas'></canvas>
</div>

object-fit: get resulting dimensions

When using the new CSS feature object-fit, how can I access the resulting dimensions that the browser has chosen by JavaScript?
So let's assume foo.jpg is 100x200 pixels. The browser page / viewport is 400px wide and 300px high. Then given this CSS code:
img.foo {
width: 100%;
height: 100%;
object-fit: contain;
object-position: 25% 0;
}
The browser would now show the image on the very top with correct aspect ration stretching to the very bottom on the second quarter from the left. This results in those image dimensions:
width: 150px
height: 300px
left: 62.5px
right: 212.5px
So what JavaScript call (jQuery allowed) would give me those numbers that I've calculated manually?
(Note: the CSS information themselves are not known by the JavaScript as the user could overwrite them and even add stuff like min-width)
To play with the code I've created a fiddle: https://jsfiddle.net/sydeo244/
Thanks to #bfred I didn't have to make the initial method.
Here is an extended (and rewritten) version of his, that does calculate the object-position values as well.
function getRenderedSize(contains, cWidth, cHeight, width, height, pos){
var oRatio = width / height,
cRatio = cWidth / cHeight;
return function() {
if (contains ? (oRatio > cRatio) : (oRatio < cRatio)) {
this.width = cWidth;
this.height = cWidth / oRatio;
} else {
this.width = cHeight * oRatio;
this.height = cHeight;
}
this.left = (cWidth - this.width)*(pos/100);
this.right = this.width + this.left;
return this;
}.call({});
}
function getImgSizeInfo(img) {
var pos = window.getComputedStyle(img).getPropertyValue('object-position').split(' ');
return getRenderedSize(true,
img.width,
img.height,
img.naturalWidth,
img.naturalHeight,
parseInt(pos[0]));
}
document.querySelector('#foo').addEventListener('load', function(e) {
console.log(getImgSizeInfo(e.target));
});
#container {
width: 400px;
height: 300px;
border: 1px solid blue;
}
#foo {
width: 100%;
height: 100%;
object-fit: contain;
object-position: 25% 0;
}
<div id="container">
<img id="foo" src="http://dummyimage.com/100x200/000/fff.jpg"/>
</div>
Side note
It appears that object-position can have more than 2 values, and when, you need to adjust (or add) which parameter returns the left position value
There's an npm package called intrinsic-scale that will calculate that for you, but it doesn't support the equivalent of object-position: https://www.npmjs.com/package/intrinsic-scale
This is the whole code:
// adapted from: https://www.npmjs.com/package/intrinsic-scale
function getObjectFitSize(contains /* true = contain, false = cover */, containerWidth, containerHeight, width, height){
var doRatio = width / height;
var cRatio = containerWidth / containerHeight;
var targetWidth = 0;
var targetHeight = 0;
var test = contains ? (doRatio > cRatio) : (doRatio < cRatio);
if (test) {
targetWidth = containerWidth;
targetHeight = targetWidth / doRatio;
} else {
targetHeight = containerHeight;
targetWidth = targetHeight * doRatio;
}
return {
width: targetWidth,
height: targetHeight,
x: (containerWidth - targetWidth) / 2,
y: (containerHeight - targetHeight) / 2
};
}
And the usage would be:
getObjectFitSize(true, img.width, img.height, img.naturalWidth, img.naturalHeight);
Here is a more comprehensive algorithm, tested, in order to determine the way the image is displayed on the screen.
var imageComputedStyle = window.getComputedStyle(image);
var imageObjectFit = imageComputedStyle.getPropertyValue("object-fit");
coordinates = {};
var imagePositions = imageComputedStyle.getPropertyValue("object-position").split(" ");
var horizontalPercentage = parseInt(imagePositions[0]) / 100;
var verticalPercentage = parseInt(imagePositions[1]) / 100;
var naturalRatio = image.naturalWidth / image.naturalHeight;
var visibleRatio = image.width / image.height;
if (imageObjectFit === "none")
{
coordinates.sourceWidth = image.width;
coordinates.sourceHeight = image.height;
coordinates.sourceX = (image.naturalWidth - image.width) * horizontalPercentage;
coordinates.sourceY = (image.naturalHeight - image.height) * verticalPercentage;
coordinates.destinationWidthPercentage = 1;
coordinates.destinationHeightPercentage = 1;
coordinates.destinationXPercentage = 0;
coordinates.destinationYPercentage = 0;
}
else if (imageObjectFit === "contain" || imageObjectFit === "scale-down")
{
// TODO: handle the "scale-down" appropriately, once its meaning will be clear
coordinates.sourceWidth = image.naturalWidth;
coordinates.sourceHeight = image.naturalHeight;
coordinates.sourceX = 0;
coordinates.sourceY = 0;
if (naturalRatio > visibleRatio)
{
coordinates.destinationWidthPercentage = 1;
coordinates.destinationHeightPercentage = (image.naturalHeight / image.height) / (image.naturalWidth / image.width);
coordinates.destinationXPercentage = 0;
coordinates.destinationYPercentage = (1 - coordinates.destinationHeightPercentage) * verticalPercentage;
}
else
{
coordinates.destinationWidthPercentage = (image.naturalWidth / image.width) / (image.naturalHeight / image.height);
coordinates.destinationHeightPercentage = 1;
coordinates.destinationXPercentage = (1 - coordinates.destinationWidthPercentage) * horizontalPercentage;
coordinates.destinationYPercentage = 0;
}
}
else if (imageObjectFit === "cover")
{
if (naturalRatio > visibleRatio)
{
coordinates.sourceWidth = image.naturalHeight * visibleRatio;
coordinates.sourceHeight = image.naturalHeight;
coordinates.sourceX = (image.naturalWidth - coordinates.sourceWidth) * horizontalPercentage;
coordinates.sourceY = 0;
}
else
{
coordinates.sourceWidth = image.naturalWidth;
coordinates.sourceHeight = image.naturalWidth / visibleRatio;
coordinates.sourceX = 0;
coordinates.sourceY = (image.naturalHeight - coordinates.sourceHeight) * verticalPercentage;
}
coordinates.destinationWidthPercentage = 1;
coordinates.destinationHeightPercentage = 1;
coordinates.destinationXPercentage = 0;
coordinates.destinationYPercentage = 0;
}
else
{
if (imageObjectFit !== "fill")
{
console.error("unexpected 'object-fit' attribute with value '" + imageObjectFit + "' relative to");
}
coordinates.sourceWidth = image.naturalWidth;
coordinates.sourceHeight = image.naturalHeight;
coordinates.sourceX = 0;
coordinates.sourceY = 0;
coordinates.destinationWidthPercentage = 1;
coordinates.destinationHeightPercentage = 1;
coordinates.destinationXPercentage = 0;
coordinates.destinationYPercentage = 0;
}
where image is the HTML <img> element and coordinates contains the following attributes, given that we consider sourceFrame being the rectangle defined by the image if it were totally printed, i.e. its natural dimensions, and printFrame being the actual displayed region, i.e. printFrame.width = image.width and printFrame.height = image.height:
sourceX: the horizontal position of the left-top point where the sourceFrame should be cut,
sourceY: the vertical position of the left-top point where the sourceFrame should be cut,
sourceWidth: how much horizontal space of the sourceFrame should be cut,
sourceHeight: how much vertical space of the sourceFrame should be cut,
destinationXPercentage: the percentage of the horizontal position of the left-top point on the printFrame where the image will be printed, relative to the printFrame width,
destinationYPercentage: the percentage of the vertical position of the left-top point on the printFrame where the image will be printed, relative to the printFrame height,
destinationWidthPercentage: the percentage of the printFrame width on which the image will be printed, relative to the printFrame width,
destinationHeightPercentage: the percentage of the printFrame height on which the image will be printed, relative to the printFrame height.
Sorry, the scale-down case is not handled, since its definition is not that clear.
Here is an updated piece of TypeScript code that handles all values including object-fit: scale-down and object-position both with relative, absolute, and keyword values:
type Rect = {
x: number;
y: number;
width: number;
height: number;
};
const dom2rect = (rect: DOMRect): Rect => {
const { x, y, width, height } = rect;
return { x, y, width, height };
};
const intersectRects = (a: Rect, b: Rect): Rect | null => {
const x = Math.max(a.x, b.x);
const y = Math.max(a.y, b.y);
const width = Math.min(a.x + a.width, b.x + b.width) - x;
const height = Math.min(a.y + a.height, b.y + b.height) - y;
if (width <= 0 || height <= 0) return null;
return { x, y, width, height };
};
type ObjectRects = {
container: Rect; // client-space size of container element
content: Rect; // natural size of content
positioned: Rect; // scaled rect of content relative to container element (may overlap out of container)
visible: Rect | null; // intersection of container & positioned rect
};
const parsePos = (str: string, ref: number): number => {
switch (str) {
case "left":
case "top":
return 0;
case "center":
return ref / 2;
case "right":
case "bottom":
return ref;
default:
const num = parseFloat(str);
if (str.endsWith("%")) return (num / 100) * ref;
else if (str.endsWith("px")) return num;
else
throw new Error(`unexpected unit object-position unit/value: '${str}'`);
}
};
const getObjectRects = (
image: HTMLImageElement | HTMLVideoElement
): ObjectRects => {
const style = window.getComputedStyle(image);
const objectFit = style.getPropertyValue("object-fit");
const naturalWidth =
image instanceof HTMLImageElement ? image.naturalWidth : image.videoWidth;
const naturalHeight =
image instanceof HTMLImageElement ? image.naturalHeight : image.videoHeight;
const content = { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
const container = dom2rect(image.getBoundingClientRect());
let scaleX = 1;
let scaleY = 1;
switch (objectFit) {
case "none":
break;
case "fill":
scaleX = container.width / naturalWidth;
scaleY = container.height / naturalHeight;
break;
case "contain":
case "scale-down": {
let scale = Math.min(
container.width / naturalWidth,
container.height / naturalHeight
);
if (objectFit === "scale-down") scale = Math.min(1, scale);
scaleX = scale;
scaleY = scale;
break;
}
case "cover": {
const scale = Math.max(
container.width / naturalWidth,
container.height / naturalHeight
);
scaleX = scale;
scaleY = scale;
break;
}
default:
throw new Error(`unexpected 'object-fit' value ${objectFit}`);
}
const positioned = {
x: 0,
y: 0,
width: naturalWidth * scaleX,
height: naturalHeight * scaleY,
};
const objectPos = style.getPropertyValue("object-position").split(" ");
positioned.x = parsePos(objectPos[0], container.width - positioned.width);
positioned.y = parsePos(objectPos[1], container.height - positioned.height);
const containerInner = { x: 0, y: 0, width: container.width, height: container.height };
return {
container,
content,
positioned,
visible: intersectRects(containerInner, positioned),
};
};
You can adjust the return value to only output what you need.

Trying to center an image in an HTML5 canvas after rotation [duplicate]

jQuery('#carregar').click(function() {
var canvas = document.getElementById('canvas');
var image = document.getElementById('image');
var element = canvas.getContext("2d");
element.clearRect(0, 0, canvas.width, canvas.height);
element.drawImage(image, 0, 0, 300, 300);
});
jsfiddle.net/braziel/nWyDE/
I have a problem to rotate an image 90 ° to the right or to the left.
I use an image on the canvas, the same screen will have several canvas equal to that of the example, but I left it as close as possible to the project.
I ask, how do I rotate the image 90 ° to the left or right when I click "Rotate Left" and "Rotate Right"?
I tried several codes on the internet but none worked.
You can use canvas’ context.translate & context.rotate to do rotate your image
Here’s a function to draw an image that is rotated by the specified degrees:
function drawRotated(degrees){
context.clearRect(0,0,canvas.width,canvas.height);
// save the unrotated context of the canvas so we can restore it later
// the alternative is to untranslate & unrotate after drawing
context.save();
// move to the center of the canvas
context.translate(canvas.width/2,canvas.height/2);
// rotate the canvas to the specified degrees
context.rotate(degrees*Math.PI/180);
// draw the image
// since the context is rotated, the image will be rotated also
context.drawImage(image,-image.width/2,-image.width/2);
// we’re done with the rotating so restore the unrotated context
context.restore();
}
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/6ZsCz/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var angleInDegrees=0;
var image=document.createElement("img");
image.onload=function(){
ctx.drawImage(image,canvas.width/2-image.width/2,canvas.height/2-image.width/2);
}
image.src="houseicon.png";
$("#clockwise").click(function(){
angleInDegrees+=30;
drawRotated(angleInDegrees);
});
$("#counterclockwise").click(function(){
angleInDegrees-=30;
drawRotated(angleInDegrees);
});
function drawRotated(degrees){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.translate(canvas.width/2,canvas.height/2);
ctx.rotate(degrees*Math.PI/180);
ctx.drawImage(image,-image.width/2,-image.width/2);
ctx.restore();
}
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=300 height=300></canvas><br>
<button id="clockwise">Rotate right</button>
<button id="counterclockwise">Rotate left</button>
</body>
</html>
Quickest 2D context image rotation method
A general purpose image rotation, position, and scale.
// no need to use save and restore between calls as it sets the transform rather
// than multiply it like ctx.rotate ctx.translate ctx.scale and ctx.transform
// Also combining the scale and origin into the one call makes it quicker
// x,y position of image center
// scale scale of image
// rotation in radians.
function drawImage(image, x, y, scale, rotation){
ctx.setTransform(scale, 0, 0, scale, x, y); // sets scale and origin
ctx.rotate(rotation);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
}
If you wish to control the rotation point use the next function
// same as above but cx and cy are the location of the point of rotation
// in image pixel coordinates
function drawImageCenter(image, x, y, cx, cy, scale, rotation){
ctx.setTransform(scale, 0, 0, scale, x, y); // sets scale and origin
ctx.rotate(rotation);
ctx.drawImage(image, -cx, -cy);
}
To reset the 2D context transform
ctx.setTransform(1,0,0,1,0,0); // which is much quicker than save and restore
Thus to rotate image to the left (anti clockwise) 90 deg
drawImage(image, canvas.width / 2, canvas.height / 2, 1, - Math.PI / 2);
Thus to rotate image to the right (clockwise) 90 deg
drawImage(image, canvas.width / 2, canvas.height / 2, 1, Math.PI / 2);
Example draw 500 images translated rotated scaled
var image = new Image;
image.src = "https://i.stack.imgur.com/C7qq2.png?s=328&g=1";
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
document.body.appendChild(canvas);
var w,h;
function resize(){ w = canvas.width = innerWidth; h = canvas.height = innerHeight;}
resize();
window.addEventListener("resize",resize);
function rand(min,max){return Math.random() * (max ?(max-min) : min) + (max ? min : 0) }
function DO(count,callback){ while (count--) { callback(count) } }
const sprites = [];
DO(500,()=>{
sprites.push({
x : rand(w), y : rand(h),
xr : 0, yr : 0, // actual position of sprite
r : rand(Math.PI * 2),
scale : rand(0.1,0.25),
dx : rand(-2,2), dy : rand(-2,2),
dr : rand(-0.2,0.2),
});
});
function drawImage(image, spr){
ctx.setTransform(spr.scale, 0, 0, spr.scale, spr.xr, spr.yr); // sets scales and origin
ctx.rotate(spr.r);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
}
function update(){
var ihM,iwM;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,w,h);
if(image.complete){
var iw = image.width;
var ih = image.height;
for(var i = 0; i < sprites.length; i ++){
var spr = sprites[i];
spr.x += spr.dx;
spr.y += spr.dy;
spr.r += spr.dr;
iwM = iw * spr.scale * 2 + w;
ihM = ih * spr.scale * 2 + h;
spr.xr = ((spr.x % iwM) + iwM) % iwM - iw * spr.scale;
spr.yr = ((spr.y % ihM) + ihM) % ihM - ih * spr.scale;
drawImage(image,spr);
}
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
The other solution works great for square images. Here is a solution that will work for an image of any dimension. The canvas will always fit the image rather than the other solution which may cause portions of the image to be cropped off.
var canvas;
var angleInDegrees=0;
var image=document.createElement("img");
image.onload=function(){
drawRotated(0);
}
image.src="http://greekgear.files.wordpress.com/2011/07/bob-barker.jpg";
$("#clockwise").click(function(){
angleInDegrees+=90 % 360;
drawRotated(angleInDegrees);
});
$("#counterclockwise").click(function(){
if(angleInDegrees == 0)
angleInDegrees = 270;
else
angleInDegrees-=90 % 360;
drawRotated(angleInDegrees);
});
function drawRotated(degrees){
if(canvas) document.body.removeChild(canvas);
canvas = document.createElement("canvas");
var ctx=canvas.getContext("2d");
canvas.style.width="20%";
if(degrees == 90 || degrees == 270) {
canvas.width = image.height;
canvas.height = image.width;
} else {
canvas.width = image.width;
canvas.height = image.height;
}
ctx.clearRect(0,0,canvas.width,canvas.height);
if(degrees == 90 || degrees == 270) {
ctx.translate(image.height/2,image.width/2);
} else {
ctx.translate(image.width/2,image.height/2);
}
ctx.rotate(degrees*Math.PI/180);
ctx.drawImage(image,-image.width/2,-image.height/2);
document.body.appendChild(canvas);
}
http://jsfiddle.net/6ZsCz/1588/
This is the simplest code to draw a rotated and scaled image:
function drawImage(ctx, image, x, y, w, h, degrees){
ctx.save();
ctx.translate(x+w/2, y+h/2);
ctx.rotate(degrees*Math.PI/180.0);
ctx.translate(-x-w/2, -y-h/2);
ctx.drawImage(image, x, y, w, h);
ctx.restore();
}
As #markE mention in his answer
the alternative is to untranslate & unrotate after drawing
It is much faster than context save and restore.
Here is an example
// translate and rotate
this.context.translate(x,y);
this.context.rotate(radians);
this.context.translate(-x,-y);
this.context.drawImage(...);
// untranslate and unrotate
this.context.translate(x, y);
this.context.rotate(-radians);
this.context.translate(-x,-y);
This is full degree image rotation code. I recommend you to check the below example app in the jsfiddle.
https://jsfiddle.net/casamia743/xqh48gno/
The process flow of this example app is
load Image, calculate boundaryRad
create temporary canvas
move canvas context origin to joint position of the projected rect
rotate canvas context with input degree amount
use canvas.toDataURL method to make image blob
using image blob, create new Image element and render
function init() {
...
image.onload = function() {
app.boundaryRad = Math.atan(image.width / image.height);
}
...
}
/**
* NOTE : When source rect is rotated at some rad or degrees,
* it's original width and height is no longer usable in the rendered page.
* So, calculate projected rect size, that each edge are sum of the
* width projection and height projection of the original rect.
*/
function calcProjectedRectSizeOfRotatedRect(size, rad) {
const { width, height } = size;
const rectProjectedWidth = Math.abs(width * Math.cos(rad)) + Math.abs(height * Math.sin(rad));
const rectProjectedHeight = Math.abs(width * Math.sin(rad)) + Math.abs(height * Math.cos(rad));
return { width: rectProjectedWidth, height: rectProjectedHeight };
}
/**
* #callback rotatedImageCallback
* #param {DOMString} dataURL - return value of canvas.toDataURL()
*/
/**
* #param {HTMLImageElement} image
* #param {object} angle
* #property {number} angle.degree
* #property {number} angle.rad
* #param {rotatedImageCallback} cb
*
*/
function getRotatedImage(image, angle, cb) {
const canvas = document.createElement('canvas');
const { degree, rad: _rad } = angle;
const rad = _rad || degree * Math.PI / 180 || 0;
debug('rad', rad);
const { width, height } = calcProjectedRectSizeOfRotatedRect(
{ width: image.width, height: image.height }, rad
);
debug('image size', image.width, image.height);
debug('projected size', width, height);
canvas.width = Math.ceil(width);
canvas.height = Math.ceil(height);
const ctx = canvas.getContext('2d');
ctx.save();
const sin_Height = image.height * Math.abs(Math.sin(rad))
const cos_Height = image.height * Math.abs(Math.cos(rad))
const cos_Width = image.width * Math.abs(Math.cos(rad))
const sin_Width = image.width * Math.abs(Math.sin(rad))
debug('sin_Height, cos_Width', sin_Height, cos_Width);
debug('cos_Height, sin_Width', cos_Height, sin_Width);
let xOrigin, yOrigin;
if (rad < app.boundaryRad) {
debug('case1');
xOrigin = Math.min(sin_Height, cos_Width);
yOrigin = 0;
} else if (rad < Math.PI / 2) {
debug('case2');
xOrigin = Math.max(sin_Height, cos_Width);
yOrigin = 0;
} else if (rad < Math.PI / 2 + app.boundaryRad) {
debug('case3');
xOrigin = width;
yOrigin = Math.min(cos_Height, sin_Width);
} else if (rad < Math.PI) {
debug('case4');
xOrigin = width;
yOrigin = Math.max(cos_Height, sin_Width);
} else if (rad < Math.PI + app.boundaryRad) {
debug('case5');
xOrigin = Math.max(sin_Height, cos_Width);
yOrigin = height;
} else if (rad < Math.PI / 2 * 3) {
debug('case6');
xOrigin = Math.min(sin_Height, cos_Width);
yOrigin = height;
} else if (rad < Math.PI / 2 * 3 + app.boundaryRad) {
debug('case7');
xOrigin = 0;
yOrigin = Math.max(cos_Height, sin_Width);
} else if (rad < Math.PI * 2) {
debug('case8');
xOrigin = 0;
yOrigin = Math.min(cos_Height, sin_Width);
}
debug('xOrigin, yOrigin', xOrigin, yOrigin)
ctx.translate(xOrigin, yOrigin)
ctx.rotate(rad);
ctx.drawImage(image, 0, 0);
if (DEBUG) drawMarker(ctx, 'red');
ctx.restore();
const dataURL = canvas.toDataURL('image/jpg');
cb(dataURL);
}
function render() {
getRotatedImage(app.image, {degree: app.degree}, renderResultImage)
}
I built an image uploader that resizes the image in the background, and also allows the user to rotate the image 90 degrees left or right. This isn't something that is solved with just a few lines of code, so you will want to checkout the jsfiddle https://jsfiddle.net/alienbush/z02jatnr/6/. I also included the complete code below.
The main drawing function looks like this:
let drawOptimizedImage = function (canvas, image, maxSize, rotationDirection) {
let degrees = updateRotationDegrees(rotationDirection)
let newSize = determineSize(image.width, image.height, maxSize.width, maxSize.height, degrees)
canvas.width = newSize.width
canvas.height = newSize.height
let ctx = canvas.getContext('2d')
ctx.save()
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (degrees === 0) {
ctx.drawImage(image, 0, 0, newSize.width, newSize.height)
}
else {
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate(degrees * Math.PI / 180)
if (Math.abs(degrees) === 180) {
ctx.drawImage(image, -newSize.width / 2, -newSize.height / 2, newSize.width, newSize.height)
}
else { // 90 or 270 degrees (values for width and height are swapped for these rotation positions)
ctx.drawImage(image, -newSize.height / 2, -newSize.width / 2, newSize.height, newSize.width)
}
}
ctx.restore()
}
Here's the complete code:
let imgPreview = document.getElementById('imgPreview')
let canvas = document.getElementById('canvas')
let statusMessage = document.getElementById('statusMessage')
let img = new Image
let maxSize = {
width: 800,
height: 600
}
let rotationDegrees = 0
// canvas.toBlob Polyfill from https://gist.github.com/salzhrani/02a6e807f24785a4d34b
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var bin = atob(this.toDataURL(type, quality).split(',')[1]),
len = bin.length,
len32 = len >> 2,
a8 = new Uint8Array(len),
a32 = new Uint32Array(a8.buffer, 0, len32);
for (var i = 0, j = 0; i < len32; i++) {
a32[i] = bin.charCodeAt(j++) |
bin.charCodeAt(j++) << 8 |
bin.charCodeAt(j++) << 16 |
bin.charCodeAt(j++) << 24;
}
var tailLength = len & 3;
while (tailLength--) {
a8[j] = bin.charCodeAt(j++);
}
callback(new Blob([a8], {
'type': type || 'image/png'
}));
}
});
}
document.getElementById('fileInput').addEventListener('change', function(e) {
if (!e.target.files.length) {
return
}
let file = e.target.files[0]
if (isValidMIME(file, ['image/bmp', 'image/jpeg', 'image/png'])) {
img.src = window.URL.createObjectURL(file)
} else {
alert('Invalid file type. The image file must be one of the following: .jpg .jpeg .png .bmp')
}
})
let isValidMIME = function(file, MIMEtypes) {
for (let i = 0; i < MIMEtypes.length; i++) {
if (MIMEtypes[i] === file.type) {
return true
}
}
return false
}
img.addEventListener('load', function() {
rotationDegrees = 0
removeStatusMessage()
drawOptimizedImage(canvas, img, maxSize)
updateImgPreview(canvas, imgPreview)
})
let removeStatusMessage = function() {
statusMessage.textContent = ''
statusMessage.style.display = 'none'
}
let drawOptimizedImage = function(canvas, image, maxSize, rotationDirection) {
let degrees = updateRotationDegrees(rotationDirection)
let newSize = determineSize(image.width, image.height, maxSize.width, maxSize.height, degrees)
canvas.width = newSize.width
canvas.height = newSize.height
let ctx = canvas.getContext('2d')
ctx.save()
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (degrees === 0) {
ctx.drawImage(image, 0, 0, newSize.width, newSize.height)
} else {
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate(degrees * Math.PI / 180)
if (Math.abs(degrees) === 180) {
ctx.drawImage(image, -newSize.width / 2, -newSize.height / 2, newSize.width, newSize.height)
} else { // 90 or 270 degrees (values for width and height are swapped for these rotation positions)
ctx.drawImage(image, -newSize.height / 2, -newSize.width / 2, newSize.height, newSize.width)
}
}
ctx.restore()
}
let updateRotationDegrees = function(rotationDirection) {
if (rotationDirection === 'clockwise') {
rotationDegrees += 90
} else if (rotationDirection === 'anticlockwise') {
rotationDegrees -= 90
}
if (Math.abs(rotationDegrees) === 360) {
rotationDegrees = 0
}
return rotationDegrees
}
let determineSize = function(width, height, maxW, maxH, degrees) {
let w, h;
degrees = Math.abs(degrees)
if (degrees === 90 || degrees === 270) { // values for width and height are swapped for these rotation positions
w = height
h = width
} else {
w = width
h = height
}
if (w > h) {
if (w > maxW) {
h = h * maxW / w
w = maxW
}
} else {
if (h > maxH) {
w = w * maxH / h
h = maxH
}
}
return {
width: w,
height: h
}
}
let updateImgPreview = function(canvas, div) {
if (canvas.width < div.clientWidth && canvas.height < div.clientHeight) {
div.style.backgroundSize = 'auto'
} else {
div.style.backgroundSize = 'contain'
}
div.style.backgroundImage = 'url(' + canvas.toDataURL() + ')'
}
document.getElementById('clockwiseBtn').addEventListener('click', function() {
removeStatusMessage()
drawOptimizedImage(canvas, img, maxSize, 'clockwise')
updateImgPreview(canvas, imgPreview)
})
document.getElementById('anticlockwiseBtn').addEventListener('click', function() {
removeStatusMessage()
drawOptimizedImage(canvas, img, maxSize, 'anticlockwise')
updateImgPreview(canvas, imgPreview)
})
document.getElementById('uploadBtn').addEventListener('click', function() {
let fileInput = document.getElementById('fileInput')
if (!fileInput.files.length) {
alert('Please choose a file first');
return;
}
let formData = new FormData()
formData.append('fileName', 'yourCustomFileNameHere')
canvas.toBlob(function(blob) {
formData.append('image', blob)
let url = 'theURLtoSendTheFileTo'
sendForm(url, formData, doAfterUploadSuccess)
}, 'image/jpeg', 1.0)
})
let sendForm = function(url, formData, callback) {
// Simulating upload. Use the commented code below for a real upload.
statusMessage.style.display = 'block'
statusMessage.textContent = 'Uploading, please wait...'
setTimeout(callback, 2000)
// let xhr = new XMLHttpRequest()
// addUploadListeners(xhr)
// xhr.open('POST', url, true)
// xhr.onload = function () {
// if (xhr.status == 200) {
// if (callback) { callback(xhr) }
// }
// else if (xhr.status === 0) {
// alert('No response from server. Check network connection.')
// }
// else {
// alert('There was a problem uploading: ' + xhr.statusText)
// }
// }
// statusMessage.style.display = 'block'
// xhr.send(formData)
}
let addUploadListeners = function(xhr) {
xhr.addEventListener('loadstart', function(e) {
statusMessage.textContent = 'Uploading, please wait...'
})
xhr.addEventListener('abort', function(e) {
statusMessage.textContent = 'Aborted upload'
})
xhr.addEventListener('error', function(e) {
statusMessage.textContent = 'Error during upload'
})
}
let doAfterUploadSuccess = function(xhr) {
statusMessage.textContent = 'Success!'
}
body {
background-color: lightgray;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 11pt;
font-weight: 400;
-webkit-font-smoothing: antialiased;
margin: 0;
}
button {
outline: none !important;
border-style: none;
background-color: rgb(115, 115, 250);
color: white;
padding: 10px 15px 12px 15px;
cursor: pointer;
border-radius: 2px;
font: inherit;
}
.canvas {
/* You can use display: none; to hide the canvas. The image will upload the same, whether or not the canvas is diplayed. */
display: block;
margin: 0px auto 40px auto;
}
.card {
background: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.img-form {
padding: 15px 15px 0px 15px;
width: 320px;
margin: 0 auto;
}
.img-uploader-panel {
padding: 10px;
width: 340px;
margin: 0px auto 20px auto;
}
.img-uploader-header {
margin-top: 5px;
text-align: center;
}
.img-preview {
display: flex;
background-repeat: no-repeat;
background-position-x: center;
background-position-y: center;
background-size: contain;
border: 1px solid rgb(160, 160, 160);
width: 320px;
height: 320px;
margin: 0 auto;
}
.status-message {
display: none;
align-self: flex-end;
margin: 0;
line-height: 50px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
width: 100%;
text-align: center;
}
.uploader-btn-panel {
display: flex;
padding-top: 10px;
justify-content: center;
align-items: center;
}
.rotate-btn {
font-size: 32px;
padding: 5px 12px 9px 13px;
margin-right: 10px;
background-color: transparent;
color: rgba(83, 83, 249, 1);
}
.clockwise-btn {
transform: rotate(90deg);
}
.anticlockwise-btn {
transform: rotate(270deg);
}
.upload-btn {
margin-left: 20px;
}
<form action="post" id="imgForm" class="img-form">
<label for="fileInput">Add an image:</label><br>
<input type="file" name="fileInput" id="fileInput" accept=".jpg, .jpeg, .png, .bmp">
</form><br>
<div class="img-uploader-panel card">
<p class="img-uploader-header">Image Preview</p>
<div id="imgPreview" class="img-preview">
<p id="statusMessage" class="status-message"></p>
</div>
<div class="uploader-btn-panel">
<button id="anticlockwiseBtn" class="anticlockwise-btn rotate-btn">↺</button>
<button id="clockwiseBtn" class="clockwise-btn rotate-btn">↻</button>
<button id="uploadBtn" class="upload-btn">Upload</button>
</div>
</div>
<canvas id="canvas" class="canvas"></canvas>
Here is Something I did
var ImgRotator = {
angle:parseInt(45),
image:{},
src:"",
canvasID:"",
intervalMS:parseInt(500),
jump:parseInt(5),
start_action:function(canvasID, imgSrc, interval, jumgAngle){
ImgRotator.jump = jumgAngle;
ImgRotator.intervalMS = interval;
ImgRotator.canvasID = canvasID;
ImgRotator.src = imgSrc ;
var image = new Image();
var canvas = document.getElementById(ImgRotator.canvasID);
image.onload = function() {
ImgRotator.image = image;
canvas.height = canvas.width = Math.sqrt( image.width* image.width+image.height*image.height);
window.setInterval(ImgRotator.keepRotating,ImgRotator.intervalMS);
//theApp.keepRotating();
};
image.src = ImgRotator.src;
},
keepRotating:function(){
ImgRotator.angle+=ImgRotator.jump;
var canvas = document.getElementById(ImgRotator.canvasID);
var ctx = canvas.getContext("2d");
ctx.save();
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.translate(canvas.width/2,canvas.height/2);
ctx.rotate(ImgRotator.angle*Math.PI/180);
ctx.drawImage(ImgRotator.image, -ImgRotator.image.width/2,-ImgRotator.image.height/2);
ctx.restore();
}
}
usage
ImgRotator.start_action("canva",
"data:image/jpeg;base64,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",
500,15
);
HTML
<canvas id="canva" width="350" height="350" style="border:solid thin black;"></canvas>
#Steve Farthing's answer is absolutely right.
But if you rotate more than 4 times then the image will get cropped from both the side.
For that you need to do like this.
$("#clockwise").click(function(){
angleInDegrees+=90 % 360;
drawRotated(angleInDegrees);
if(angleInDegrees == 360){ // add this lines
angleInDegrees = 0
}
});
Then you will get the desired result. Thanks. Hope this will help someone :)
Why not do it for the entire page. At page load detect all images and continuously rotate all of them.
var RotationCollection = {
rotators: [],
start_action: function (showBorders, isoverlap) {
try {
var canvasTemplate = '<canvas id="_ID_" width="350" height="350" ></canvas>';
var ja = 5;
$.each($("img"), function (index, val) {
var newID = "can_" + index;
var can = canvasTemplate.replace("_ID_", newID);
if (showBorders == true) $(can).insertAfter($(val)).css({ "border": "solid thin black", "box-shadow": "5px 5px 10px 2px black", "border-raduis": "15px" });
else $(can).insertAfter($(val));
$(val).remove();
var curRot = new RotationClass(newID, $(val).attr('src'), ja * ((0 == index % 2) ? -1 : 1), isoverlap);
RotationCollection.rotators[index] = curRot;
ja += 5;
//return false;
});
window.setInterval(function () {
$.each(RotationCollection.rotators, function (index, value) {
value.drawRotatedImage();
})
}, 500);
}
catch (err) {
console.log(err.message);
}
}
};
function RotationClass(canvasID, imgSrc, jumgAngle, overlap) {
var self = this;
self.overlap = overlap;
self.angle = parseInt(45);
self.image = {};
self.src = imgSrc;
self.canvasID = canvasID;
self.jump = parseInt(jumgAngle);
self.start_action = function () {
var image = new Image();
var canvas = document.getElementById(self.canvasID);
image.onload = function () {
self.image = image;
canvas.height = canvas.width = Math.sqrt(image.width * image.width + image.height * image.height);
self.drawRotatedImage(self);
};
image.src = self.src;
}
self.start_action();
this.drawRotatedImage = function () {
var self = this;
self.angle += self.jump;
var canvas = document.getElementById(self.canvasID);
var ctx = canvas.getContext("2d");
ctx.save();
if (self.overlap) ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(self.angle * Math.PI / 180);
ctx.drawImage(self.image, -self.image.width / 2, -self.image.height / 2);
ctx.restore();
}
}
var theApp = {
start_Action: function () {
RotationCollection.start_action(true, true);
}
};
$(document).ready(theApp.start_Action);
Please check out for theApp.start_Action where all action begins
The HTML can be as follows:
<p>
Deepika Padukone.<br />
<img alt="deepika" src="data:image/jpeg;base64,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" />
</p>
<p>
Priyanka Chopra.<br />
<img alt="Priyanka" src="data:image/jpeg;base64,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" />
</p>
Some options to overlap rotations, borders are also added
To rotate and fill (stretch to fit), try this fiddle which uses the corner of the canvas for a start point instead of the center and can rotate 90, 180 and 270 degrees.
Note: the fiddle is stretching a 200 * 300 image to fit a 600 * 650 canvas
const canvas = document.getElementById('canvas');
const image = document.getElementById('source');
const Rotation = {
None: 0,
Right: 90,
Flip: 180,
Left: 270
};
const noRotation = () => Rotate(Rotation.None);
const rotateRight = () => Rotate(Rotation.Right);
const rotateLeft = () => Rotate(Rotation.Left);
const flip = () => Rotate(Rotation.Flip);
const Rotate = (orientation) => rotateImage(canvas, image, orientation);
function rotateImage(canvas, image, rotation){
const rotate = correctRotation(rotation);
const context = canvas.getContext('2d');
let offsetX = 0;
let offsetY = 0;
let displacementX = canvas.width;
let displacementY = canvas.height;
if (rotate === Rotation.Left || rotate === Rotation.Right){
displacementX = canvas.height;
displacementY = canvas.width;
}
switch(rotate) {
case Rotation.Flip:
offsetX = canvas.width;
offsetY = canvas.height;
break;
case Rotation.Left:
offsetY = canvas.height;
break;
case Rotation.Right:
offsetX = canvas.width;
break;
}
context.setTransform(1, 0, 0, 1, offsetX, offsetY);
context.rotate(rotate * Math.PI / 180);
context.drawImage(image, 0,0, displacementX, displacementY )
}
function correctRotation(rotation) {
return !rotation ? Rotation.None
: rotation < Rotation.Flip ? Rotation.Right
: rotation < Rotation.Left ? Rotation.Flip
: Rotation.Left;
}
image.addEventListener('load', e => noRotation() );
https://jsfiddle.net/0unyh7ck/8/

Categories

Resources