I ended up solving this in a roundabout way by wrapping the image in a dedicated container, along with some strange looking javascript to keep it in place as the image loads. The dimensions for that container are calculated as in Sven's answer, but ultimately it lets the browser take over. This way layout changes are kept fairly minimal and we end up only doing this crazy stuff for the bit of time between images.
Here's a big wad of code, for completedness:
function Viewer(container) {
var viewer = this;
container = $(container);
var pictureBox = $('.picture', container);
var img = $('<img>').appendTo(pictureBox);
var hide = function() {
/* [snip] */
}
var getPictureDisplayHeight = function(picture) {
var ratio = picture.data.h / picture.data.w;
var displayWidth = Math.min(pictureBox.width(), picture.data.w);
var displayHeight = Math.min(displayWidth * ratio, picture.data.h);
return displayHeight;
}
var stopLoadingTimeoutId = undefined;
var stopLoadingTimeout = function() {
container.removeClass('loading');
}
var showPicture = function(picture) {
var imgIsChanging = img.data('picture') != picture;
container.show();
/* This code expects to be cleaned up by stopLoadingTimeout or onImgLoaded, which will not fire if img src doesn't change */
if (imgIsChanging) {
container.addClass('loading');
window.clearTimeout(stopLoadingTimeoutId);
stopLoadingTimeoutId = window.setTimeout(stopLoadingTimeout, 3000);
}
pictureBox.css({
'min-height' : pictureBox.height()
});
var displayHeight = getPictureDisplayHeight(picture);
if (displayHeight > pictureBox.height()) {
/* Grow pictureBox if necessary */
pictureBox.stop(true, false);
pictureBox.animate({
'height' : displayHeight
}, 150);
}
/* I wish I could set width and height here, but it causes the current image to stretch */
img.attr({
'src' : picture.fullPath
}).data('picture', picture);
}
var onImgLoaded = function(event) {
/* The load event might not be fired, so nothing here should be essential */
var picture = img.data('picture');
container.removeClass('loading');
var displayHeight = getPictureDisplayHeight(picture);
pictureBox.stop(true, false);
pictureBox.animate({
'min-height' : 0,
'height' : displayHeight
}, 150, function() {
pictureBox.css('height', 'auto');
});
window.clearTimeout(stopLoadingTimeoutId);
}
var onImgClicked = function(event) {
selectNextPicture();
}
var onPictureSelectedCb = function(picture) {
if (picture) {
showPicture(picture);
} else {
hide();
}
}
var init = function() {
img.on('click', onImgClicked);
img.on('load', onImgLoaded);
}
init();
}
Relevant HTML:
<div class="viewer" style="display: none;">
<div class="picture"></div>
<div class="caption"><div class="caption-text"></div></div>
</div>
And CSS:
.viewer .picture img {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
}
This way we leave space around the image that is either the size of the next image or the size of the current image, and never the smaller size that seems to happen before a new image is loaded (which kept happening for some reason). There are probably a million solutions to this, and mine doesn't feel especially straight-forward, so I'm certainly curious to see others :)