From a4f5ee4a22457e3d8098ae65a4e97aa856184a84 Mon Sep 17 00:00:00 2001 From: fkakatie Date: Wed, 15 Jul 2026 17:40:15 -0600 Subject: [PATCH] feat: contain images in columns --- blocks/columns/columns.css | 4 +++ blocks/columns/columns.js | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index a19a6d2..b81a27e 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -55,6 +55,10 @@ object-fit: cover; } +.columns .media-wrapper img.contain { + object-fit: contain; +} + .columns .media-wrapper > video { position: absolute; inset: 0; diff --git a/blocks/columns/columns.js b/blocks/columns/columns.js index 107fe4d..93386cb 100644 --- a/blocks/columns/columns.js +++ b/blocks/columns/columns.js @@ -25,6 +25,56 @@ function optimizeImages(block) { }); } +/** + * Checks whether a loaded image should be shown with `contain` instead of `cover`. + * @param {HTMLImageElement} img The loaded image element + * @returns {boolean} `true` if the image has any non-opaque pixels or a solid white background + */ +function shouldContain(img) { + try { + const size = 100; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0, size, size); + const { data } = context.getImageData(0, 0, size, size); + + const isWhite = (i) => data[i + 3] === 255 + && data[i] >= 250 && data[i + 1] >= 250 && data[i + 2] >= 250; + + const corners = [ + 0, + (size - 1) * 4, + (size - 1) * size * 4, + ((size - 1) * size + (size - 1)) * 4, + ]; + + if (corners.every(isWhite)) return true; + + for (let i = 3; i < data.length; i += 4) { + if (data[i] < 255) return true; + } + return false; + } catch { + return false; + } +} + +/** + * Adds a `contain` class to images that should not be cropped. + * @param {Element} block The columns block element + */ +function detectContainMedia(block) { + block.querySelectorAll('.media-wrapper picture > img').forEach((img) => { + const check = () => { + if (shouldContain(img)) img.classList.add('contain'); + }; + if (img.complete) check(); + else img.addEventListener('load', check, { once: true }); + }); +} + /** * Marks body-only blocks whose last column is links-only with `action` and `action-wrapper`. * @param {Element} block The columns block element @@ -75,4 +125,5 @@ export default function decorate(block) { } optimizeImages(block); + detectContainMedia(block); }