I'm drawing a simple panel (using Phaser.Graphics) which will contain a list of pre-defined amount of list-entries (also drawn usingPhaser.Graphics).
The size of each list-entry is dynamically calculated to fit the panel size.
Width of my list-entries are (+-) same as panel's width.
To get the height, I'm dividing panel's height by the number of list-entries and their margins size.
The result is almost accurate, but I'm still getting either some extra or missing some pixels below the last list-entry. So I suppose my calculation isn't correct or missing something ...
var game = new Phaser.Game(400, 300, Phaser.AUTO, 'phaser-example', {
create: create
});
var panel = null;
var listItems = 3
var listEntries = [];
function create() {
createPanel(game.world.centerX, game.world.centerY, game.world.width - 50, game.world.height - 100);
createList(panel);
}
function createPanel(x, y, width, height) {
panel = game.make.graphics(0, 0);
panel.position.x = x - (width / 2);
panel.position.y = y - (height / 2);
panel.lineStyle(2, 0x999999, 0.4);
panel.beginFill(0x0d1a26, 0.6);
panel.drawRect(0, 0, width, height);
panel.endFill();
game.world.add(panel)
}
function createList(parent) {
let child;
let margin = 5;
let width = parent.width - parent.lineWidth; //// - borders width? ////
let height = (parent.height - ( listItems * (margin * 2) )) / listItems;
let centerX = ((parent.width - width) / 2) - 1; /// - left-border width? ///
let prev_pos = 0;
for (let e = 0, e_l = listItems; e < e_l; e++) {
listEntries[e] = this.game.make.graphics(0, 0);
createListEntry(listEntries[e], width, height);
child = parent.addChild(listEntries[e]);
child.position.x += centerX;
if (e > 0) {
child.position.y += prev_pos + (margin * 2);
} else {
child.position.y += prev_pos + margin;
}
prev_pos = child.position.y + height;
console.log(child.position.y)
}
}
function createListEntry(entry, width, height) {
entry.clear();
entry.lineStyle(2, 0x999999, 0.5);
entry.beginFill(0x0d1a26, 0.7);
entry.drawRect(0, 0, width, height);
entry.endFill();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/2.6.2/phaser.min.js"></script>
<html>
<head>
<style>
canvas {
position: relative;
margin: 0 auto;
}
</style>
</head>
<body>
</body>
</html>
Integers for pixels.
From memory Phaser (the version that was about 2-3 years ago) would use an optimisation in javascript of converting all rendering coordinates to integers.
If you give it doubles it will floor them (or round I can't remember which). As you have a fraction that you carry through your position prev_pos is not matching the actual placement of the panels.
The easy fix is to only give Phaser integers. In this case round up with prev_pos = Math.ceil(child.position.y + height);
Fixed?
Your code with the minor change commented.
var game = new Phaser.Game(400, 300, Phaser.AUTO, 'phaser-example', {
create: create
});
var panel = null;
var listItems = 3
var listEntries = [];
function create() {
createPanel(game.world.centerX, game.world.centerY, game.world.width - 50, game.world.height - 100);
createList(panel);
}
function createPanel(x, y, width, height) {
panel = game.make.graphics(0, 0);
panel.position.x = x - (width / 2);
panel.position.y = y - (height / 2);
panel.lineStyle(2, 0x999999, 0.4);
panel.beginFill(0x0d1a26, 0.6);
panel.drawRect(0, 0, width, height);
panel.endFill();
game.world.add(panel)
}
function createList(parent) {
let child;
let margin = 5;
let width = parent.width - parent.lineWidth; //// - borders width? ////
let height = (parent.height - ( listItems * (margin * 2) )) / listItems;
let centerX = ((parent.width - width) / 2) - 1; /// - left-border width? ///
let prev_pos = 0;
for (let e = 0, e_l = listItems; e < e_l; e++) {
listEntries[e] = this.game.make.graphics(0, 0);
createListEntry(listEntries[e], width, height);
child = parent.addChild(listEntries[e]);
child.position.x += centerX;
if (e > 0) {
child.position.y += prev_pos + (margin * 2);
} else {
child.position.y += prev_pos + margin;
}
// The new expression
prev_pos = Math.ceil(child.position.y + height);
//.........^^^^^^^^^^.........................^
// the added code.
}
}
function createListEntry(entry, width, height) {
entry.clear();
entry.lineStyle(2, 0x999999, 0.5);
entry.beginFill(0x0d1a26, 0.7);
entry.drawRect(0, 0, width, height);
entry.endFill();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/2.6.2/phaser.min.js"></script>
<html>
<head>
<style>
canvas {
position: relative;
margin: 0 auto;
}
</style>
</head>
<body>
</body>
</html>
Related
I got a isometric grid in html canvas.
I am trying to handle the mouse hover the buildings.
Some buildings will have different heights.
As you can see in the image below I am hovering a tile, the mouse pointer is inside the blueish tile.
The problem is when the mouse pointer is off the ground tile, or in the middle of the building image, the highlighted tile goes off.
Need a way to click on each individual building, how can this be resolved?
Main basic functions:
let applied_map = ref([]); // tileMap
let tile_images = ref([]); // this will contain loaded IMAGES for canvas to consume from
let tile_height = ref(50);
let tile_width = ref(100);
const renderTiles = (x, y) => {
let tileWidth = tile_width.value;
let tileHeight = tile_height.value;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
for (let tileX = 0; tileX < gridSize.value; ++tileX) {
for (let tileY = 0; tileY < gridSize.value; ++tileY) {
let renderX = x + (tileX - tileY) * tile_half_width;
let renderY = y + (tileX + tileY) * tile_half_height;
let tile = applied_map.value[tileY * gridSize.value + tileX];
renderTileBackground(renderX, renderY + 50, tileWidth, tileHeight);
if (tile !== -1) {
if (tile_images.value.length) {
renderTexturedTile(
tile_images.value[tile].img,
renderX,
renderY + 40,
tileHeight
);
}
}
}
}
if (
hoverTileX.value >= 0 &&
hoverTileY.value >= 0 &&
hoverTileX.value < gridSize.value &&
hoverTileY.value < gridSize.value
) {
let renderX = x + (hoverTileX.value - hoverTileY.value) * tile_half_width;
let renderY = y + (hoverTileX.value + hoverTileY.value) * tile_half_height;
renderTileHover(renderX, renderY + 50, tileWidth, tileHeight);
}
};
const renderTileBackground = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([5, 5]);
ctx.value.strokeStyle = "black";
ctx.value.fillStyle = "rgba(25,34, 44,0.2)";
ctx.value.lineWidth = 1;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
const renderTexturedTile = (imgSrc, x, y, tileHeight) => {
let offsetY = tileHeight - imgSrc.height;
ctx.value.drawImage(imgSrc, x, y + offsetY);
};
const renderTileHover = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([]);
ctx.value.strokeStyle = "rgba(161, 153, 255, 0.8)";
ctx.value.fillStyle = "rgba(161, 153, 255, 0.4)";
ctx.value.lineWidth = 2;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
Updates after answer below
Based on Helder Sepulveda answer I created a function drawCube.
And added to my click function and to the renderTiles. So on click and frame update it creates a cube with 3 faces,and its placed on same position as the building and stores the Path on a global variable, the cube follows the isometric position.
In the drawCube, there is a condition where i need to hide the right face from the cube. Hide if there's a building on the next tile. So if you hover the building it wont trigger the last building on.
//...some code click function
//...
if (tile_images.value[tileIndex] !== undefined) {
drawCube(
hoverTileX.value + tile_height.value,
hoverTileY.value +
Number(tile_images.value[tileIndex].img.height / 2) -
10,
tile_height.value, // grow X pos to left
tile_height.value, // grow X pos to right,
Number(tile_images.value[tileIndex].img.height / 2), // height,
ctx.value,
{
tile_index: tileIndex - 1 < 0 ? 0 : tileIndex - 1,
}
);
}
This is the drawCube
const drawCube = (x, y, wx, wy, h, the_ctx, options = {}) => {
// https://codepen.io/AshKyd/pen/JYXEpL
let path = new Path2D();
let hide_options = {
left_face: false,
right_face: false,
top_face: false,
};
if (options.hasOwnProperty("hide")) {
hide_options = Object.assign(hide_options, options.hide);
}
// left face
if (!hide_options.left_face) {
path.moveTo(x, y);
path.lineTo(x - wx, y - wx * 0.5);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x, y - h * 1);
}
// right;
if (
!hide_options.right_face &&
!coliders.value[options.tile_index].hide_right_face
) {
path.moveTo(x, y);
path.lineTo(x + wy, y - wy * 0.5);
path.lineTo(x + wy, y - h - wy * 0.5);
path.lineTo(x, y - h * 1);
}
//top
if (!hide_options.right_face) {
path.moveTo(x, y - h);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x - wx + wy, y - h - (wx * 0.5 + wy * 0.5));
path.lineTo(x + wy, y - h - wy * 0.5);
}
// the_ctx.beginPath();
let isONHover = the_ctx.isPointInPath(
path,
mousePosition.x - 10,
mousePosition.y - 10
);
the_ctx.fillStyle = null;
if (isONHover) {
// let indx = options.tile_pos.y * gridSize.value + options.tile_pos.x;
//this is the click on object event
if (isMouseDown.value) {
//Trigger
if (buildozer.value === true) {
coliders.value[options.tile_index] = -1;
applied_map.value[options.tile_index] = -1;
}
isMouseDown.value = false;
}
the_ctx.fillStyle = "green";
}
the_ctx.fill(path);
if (
coliders.value[options.tile_index] == -1 &&
applied_map.value[options.tile_index]
) {
coliders.value[options.tile_index] = path;
}
};
In a nutshell you need to be able to detect mouseover on more complex shapes ...
I recommend you to use Path2d:
https://developer.mozilla.org/en-US/docs/Web/API/Path2D
That way you can build any shape you like and then we have access to isPointInPath to detect if the mouse is over our shape.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
Here is a small example:
class Shape {
constructor(x, y, width, height) {
this.path = new Path2D()
this.path.arc(x, y, 12, 0, 2 * Math.PI)
this.path.arc(x, y - 9, 8, 0, 1.5 * Math.PI)
this.path.lineTo(x + width / 2, y)
this.path.lineTo(x, y + height / 2)
this.path.lineTo(x - width / 2, y)
this.path.lineTo(x, y - height / 2)
this.path.lineTo(x + width / 2, y)
}
draw(ctx, pos) {
ctx.beginPath()
ctx.fillStyle = ctx.isPointInPath(this.path, pos.x, pos.y) ? "red" : "green"
ctx.fill(this.path)
}
}
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect()
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
}
}
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
shapes = []
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
shapes.push(new Shape(50 + i * 40, 40 + j * 40, 40, 20))
}
}
canvas.addEventListener("mousemove", function(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
var mousePos = getMousePos(canvas, evt)
shapes.forEach((s) => {s.draw(ctx, mousePos)})
},
false
)
shapes.forEach((s) => {
s.draw(ctx, {x: 0, y: 0})
})
<canvas id="canvas" width="200" height="200"></canvas>
This example draws a "complex" shape (two arcs and a few lines) and the shape changes color to red when the mouse is hovering the shape
Objective: Amend the compassGrid() function to create a grid of 25x25 lines of length stepSize. Make sure each compass is at the center of each tile. By default they should all be pointing up. You should use translate() to move to the center of each grid.
I've tried all kinds of stuff, but none of my attempts seem to reach the objective outlined above. Here’s my code:
var stepSize = 20;
function setup() {
createCanvas(500, 500);
}
///////////////////////////////////////////////////////////////////////
function draw() {
background(125);
colorGrid();
compassGrid();
}
///////////////////////////////////////////////////////////////////////
function colorGrid() {
for (var x = 0; x < width; x += width / 25) {
for (var y = 0; y < height; y += height / 25) {
let from = color(255, 0, 0);
let to = color(0, 255, 0);
let interA = lerpColor(from, to, n);
var n = noise(x / 50, y / 50, frameCount / 100);
var c = map(n, 0, 1, 0, 255);
fill(interA);
stroke(c);
rect(x, y, stepSize, stepSize);
}
}
}
///////////////////////////////////////////////////////////////////////
function compassGrid() {
translate(10, 10);
for (var x = 0; x < width; x += width / 25) {
for (var y = 0; y < height; y += height / 25) {
push();
translate(x, y);
stroke("black");
line(0, 0, 0, stepSize - 5);
pop();
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script>
What's my mistake?
The following question is based on an adaption I wish to make of this codepen:
Here is a Pen
I have the following html for displaying the canvas (I want it to be only in a section on the html page and not take up the whole page)
<div class="container">
<canvas id="c"></canvas>
</div>
The javascript for the canvas which shows an animation is below.
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var H = window.innerHeight;
var W = window.innerWidth;
canvas.height = H;
canvas.width = W;
var NBR_PARTICLES = 100;
var INTENSITY = 55;
var BLUE_RATIO = 5;
particles = [];
for (var i = 0; i < NBR_PARTICLES; i++) {
particles.push( new particle(i) );
};
function particle(i){
this.size = rand(0, 1.4);
this.x = W / 2;
this.y = H / 2;
this.vx = rand(-1, 1);
this.vy = rand(-1, 1);
this.decay = rand(0.9, 1);
this.c = 0;
}
function draw(){
for (var i = 0; i < NBR_PARTICLES; i++) {
p = particles[i];
ctx.fillStyle = color(p.size);
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, Math.PI*2, false);
ctx.fill();
p.size *= p.decay;
p.x += p.vx;
p.y += p.vy;
p.vx += rand(-.2, .2);
p.vy += rand(-.2, .2);
p.c++;
if(p.size < .2){
particles[i] = new particle(i);
}
};
}
function color(i){
value = 255 - Math.round( (i * (255 / NBR_PARTICLES)) * INTENSITY);
return "rgba(" + value + ", 0, " + Math.round((NBR_PARTICLES - i) / BLUE_RATIO) + ", .75)";
}
setInterval(draw, 33);
/*************************
CONSTRUCT FUNCTIONS
**************************/
function rand(min, max){
value = min + Math.random() * ( max - min );
return value;
}
function cd(args){ // FOR DEBUG
console.dir(args);
}
</script>
I want the canvas size to be a rectangular banner across the page rather than the whole page as it is now.
I have tried changing these variables
var H = window.innerHeight;
var W = window.innerWidth;
canvas.height = H;
canvas.width = W;
to
canvas.height = 200;
canvas.width = 800;
but that doesn't render the animation at all but does appear to resize the canvas.
The CSS here appears to override the existing body as the whole animation takes over the page and my existing content is no longer displayed.
body, html{
margin: 0;
padding: 0;
overflow: hidden;
background-color: #ddd;
}
I tried removing the body from the css but that didn't work at all.
I also, as you can see above, added a div container, hoping that would isolate the canvas but that hasn't worked either.
<div class="container">
<canvas id="c"></canvas>
</div>
How do I adapt this code to make the canvas only render on a portion (width and height of the canvas decided by me) on the screen.
Setting directly the height H and width W , will show canvas correctly at center :
below snippet you can see result centred annilation correctly
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
// set here canvas width and height directly
var H = 200;
var W = 200;
canvas.height = H;
canvas.width = W;
var NBR_PARTICLES = 100;
var INTENSITY = 55;
var BLUE_RATIO = 5;
particles = [];
for (var i = 0; i < NBR_PARTICLES; i++) {
particles.push( new particle(i) );
};
function particle(i){
this.size = rand(0, 1.4);
this.x = W / 2;
this.y = H / 2;
this.vx = rand(-1, 1);
this.vy = rand(-1, 1);
this.decay = rand(0.9, 1);
this.c = 0;
}
function draw(){
for (var i = 0; i < NBR_PARTICLES; i++) {
p = particles[i];
ctx.fillStyle = color(p.size);
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, Math.PI*2, false);
ctx.fill();
p.size *= p.decay;
p.x += p.vx;
p.y += p.vy;
p.vx += rand(-.2, .2);
p.vy += rand(-.2, .2);
p.c++;
if(p.size < .2){
particles[i] = new particle(i);
}
};
}
function color(i){
value = 255 - Math.round( (i * (255 / NBR_PARTICLES)) * INTENSITY);
return "rgba(" + value + ", 0, " + Math.round((NBR_PARTICLES - i) / BLUE_RATIO) + ", .75)";
}
setInterval(draw, 33);
/*************************
CONSTRUCT FUNCTIONS
**************************/
function rand(min, max){
value = min + Math.random() * ( max - min );
return value;
}
function cd(args){ // FOR DEBUG
console.dir(args);
}
body, html{
margin: 0;
padding: 0;
overflow: hidden;
background-color: #ddd;
text-align:center
}
canvas {
border : 1px solid gray;
}
<canvas id="c"></canvas>
I've read multiple suggestions on the issue of canvas blurriness on retina displays (e.g. using the window.devicePixelRatio approach; here, here and also here) but I haven't been able to apply the suggested solutions to my specific problem. The following script first creates a canvas with some random image data (which appear blurry), and then exports the image to a SVG element and rescales it (still blurry of course). I am using a MBP late 2016 with touch bar and safari. Any suggestions on how to avoid blurriness and achieve crisp edges? Keep in mind that the initial imageData should have a fixed width and height.
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<body></body>
<script type="text/javascript">
var width = 100;
var height = 100;
var canvas = d3.select("body").append("canvas");
context = canvas.node().getContext("2d"),
canvas
.attr("width", width)
.attr("height", height)
.style("width", width + "px")
.style("height", height + "px")
//this is the part that should normally take care of blurriness
if (window.devicePixelRatio > 1) {
var devicePixelRatio = window.devicePixelRatio || 1;
var backingStoreRatio = context.webkitBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
var ratio = devicePixelRatio / backingStoreRatio;
canvas
.attr('width', width * ratio)
.attr('height', height * ratio)
.style('width', width + 'px')
.style('height', height + 'px');
context.scale(ratio, ratio);
}
var imageData = context.createImageData(width, height);
for (var i = 0, l = 0; i<height; ++i) {
for (j = 0; j<width; ++j, l += 4) {
imageData.data[l+0] = Math.round( Math.random() * 255);
imageData.data[l+1] = Math.round( Math.random() * 255);
imageData.data[l+2] = Math.round( Math.random() * 255);
imageData.data[l+3] = Math.round( Math.random() * 255);
}
}
context.putImageData(imageData, 0, 0);
var ImageD = canvas.node().toDataURL("img/png");
var svg = d3.select('body').append('svg').attr('width', width*5).attr('height', height*5);
svg.append("svg:image").datum(ImageD).attr("xlink:href", function(d) {return d})
.attr("height", height*5).attr("width", width*5)
</script>
I've finally found the solution. I use a combination of the following: window.devicePixelRatio for getting the retina pixel ratio, off-screen canvas taken from here, and then scaling up the context taken from here
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<body></body>
<script type="text/javascript">
const width = 20;
const height = 20;
const scale = 10; // the higher the number the crisper the custom image
var canvas = d3.select("body").append("canvas");
context = canvas.node().getContext("2d");
const ratio = window.devicePixelRatio || 1;
canvas.attr('width', width * ratio * scale)
.attr('height', height * ratio * scale)
.style('width', width * scale + 'px')
.style('height', height * scale + 'px');
var imageData = context.createImageData(width, height);
for (var i = 0, l = 0; i<height; ++i) {
for (j = 0; j<width; ++j, l += 4) {
imageData.data[l+0] = Math.round( Math.random() * 255);
imageData.data[l+1] = Math.round( Math.random() * 255);
imageData.data[l+2] = Math.round( Math.random() * 255);
imageData.data[l+3] = Math.round( Math.random() * 255);
}
}
const offCtx = canvas.node().cloneNode().getContext('2d'); // create an off screen canvas
offCtx.putImageData(imageData, 0,0);
context.scale(ratio * scale, ratio * scale);
context.mozImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
context.drawImage(offCtx.canvas, 0,0);
//export image
var ImageD = canvas.node().toDataURL("img/png");
//load image
d3.select('body').append('svg').attr("height", 500).attr("width", 500).append("svg:image").datum(ImageD).attr("xlink:href", function(d) {return d})
.attr("height", 500).attr("width", 500);
</script>
Image data is not context aware.
The code you had would work if you used the context to render, but you are writing pixels directly to a image buffer. This is not effected by the 2D context transform and hence your code does not scale up.
In you code the line
context.scale(ratio, ratio);
that scales up the canvas rendering does not apply to the imagedata.
Simple fix
A simple fix if you know the device is retina. It doubles the canvas resolution and then sets random pixels. To keep with your original code I set 2 by 2 pixels to the same random value. The blur will be gone but the random pattern remain the same.
const width = 100;
const height = 100;
const w = width; // because I hate cluttered code
const h = height;
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
canvas.width = w * 2;
canvas.height = h * 2;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
const imageData = ctx.createImageData(w * 2, h * 2);
// get 32bit view of data
const b32 = new Uint32Array(imageData.data.buffer);
// this is the part that you need to change as the canvas resolution is double
for (let i = 0, l = 0; i< h; i ++) {
for (let j = 0; j < w; j ++) {
const idx = i * w* 2 + j * 2;
b32[idx + w + 1] = b32[idx + w] = b32[idx + 1] = b32[idx] = (Math.random() * 0xFFFFFFFF) | 0;
}
}
ctx.putImageData(imageData, 0, 0);
const ImageD = canvas.toDataURL("img/png");
const svg = d3.select('body').append('svg').attr('width', width*5).attr('height', height*5);
svg.append("svg:image").datum(ImageD).attr("xlink:href", function(d) {return d})
.attr("height", height*5).attr("width", width*5)
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.