Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Rectpackr Layout

[![NPM Version](https://img.shields.io/npm/v/rectpackr-layout)](https://www.npmjs.com/package/rectpackr-layout)
[![Coverage Status](https://img.shields.io/coverallsCoverage/github/styiannis/rectpackr-layout)](https://coveralls.io/github/styiannis/rectpackr-layout?branch=main)
[![CodePen Demos](https://img.shields.io/badge/CodePen-Demos-blue)](https://codepen.io/collection/dGpeLa)

A web component that creates layouts by treating your HTML elements as rectangles and packing them using a best-fit 2D strip-packing algorithm.

## ⚙️ Why a Packing Algorithm for Web Layouts?
Expand Down Expand Up @@ -68,6 +72,39 @@ Or directly in your HTML:
</script>
```

### Using a CDN (No Build Step Needed)

Include it directly in your HTML via CDN:

#### unpkg

```html
<script type="module" src="https://unpkg.com/rectpackr-layout"></script>
```

#### jsDelivr

```html
<script
type="module"
src="https://cdn.jsdelivr.net/npm/rectpackr-layout"
></script>
```

#### esm.sh

```html
<script type="module" src="https://esm.sh/rectpackr-layout"></script>
```

### Once installed, use the web component anywhere in your HTML:

```html
<rectpackr-layout>
<div>Your content here</div>
</rectpackr-layout>
```

## 📖 API Reference

### Attributes
Expand Down Expand Up @@ -151,6 +188,26 @@ The `x-direction` and `y-direction` attributes control visual placement, which m
</script>
```

## 🎯 Live Demos

### Consistent Width Gallery

See predictable masonry-style layouts with equal-width elements

[View on CodePen](https://codepen.io/styiannis/pen/ogbzBXg)

### Mixed Dimension Gallery

Explore optimal packing of variably-sized elements and aspect ratios

[View on CodePen](https://codepen.io/styiannis/pen/XJXjayR)

### Interactive Playground

Experiment with real-time controls and dynamic content manipulation

[View on CodePen](https://codepen.io/styiannis/pen/qEbqMBZ)

## ✅ Browser Support

Modern browsers with Web Components support.
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rectpackr-layout",
"version": "0.9.0",
"version": "1.0.0",
"description": "A web component that creates layouts by packing HTML elements as rectangles using a best-fit 2D strip-packing algorithm. Automatically measures dimensions and handles mixed content.",
"keywords": [
"best-fit",
Expand All @@ -12,7 +12,7 @@
"masonry-layout",
"responsive",
"responsive-layout",
"rectangle-packer",
"rectangle-packing",
"strip-packing",
"web-component",
"web-components"
Expand Down Expand Up @@ -63,22 +63,22 @@
"validate-exports": "node ./scripts/validate-exports.js"
},
"dependencies": {
"best-fit-strip-pack": "^1.0.1"
"best-fit-strip-pack": "^1.0.2"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-node-resolve": "^16.0.2",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4",
"@tsconfig/node22": "^22.0.2",
"@types/jest": "^30.0.0",
"@types/node": "^24.5.2",
"jest": "^30.1.3",
"jest-environment-jsdom": "^30.1.2",
"rollup": "^4.52.2",
"@types/node": "^24.7.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"rollup": "^4.52.4",
"rollup-plugin-dts": "^6.2.3",
"ts-jest": "^29.4.4",
"tslib": "^2.8.1",
"typescript": "^5.9.2"
"typescript": "^5.9.3"
},
"exports": {
".": {
Expand Down
3 changes: 2 additions & 1 deletion src/core/rectpackr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ export function create<R extends IRectpackr>(
container,
children: [] as R['children'],
childrenContainer,
isPendingStartObservingChildren: false,
loadingImages: new Map(),
observers: { childrenContainerMutation, childrenResize, containerResize },
pendingStartObservingChildren: false,
stripPack,
} as R;

Expand All @@ -47,5 +47,6 @@ export function create<R extends IRectpackr>(
export function clear<R extends IRectpackr>(instance: R) {
stopObserving(instance);
resetStyle(instance);
instance.children.length = 0;
instance.stripPack.reset();
}
2 changes: 1 addition & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ export interface IRectpackr {
width: number;
}[];
childrenContainer: HTMLElement;
isPendingStartObservingChildren: boolean;
loadingImages: Map<HTMLElement, (this: HTMLImageElement) => void>;
observers: {
childrenContainerMutation: MutationObserver;
childrenResize: ResizeObserver;
containerResize: ResizeObserver;
};
pendingStartObservingChildren: boolean;
stripPack: BestFitStripPack;
}
36 changes: 16 additions & 20 deletions src/core/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ function render(instance: IRectpackr) {
}

function restartObservingChildren(instance: IRectpackr) {
if (instance.pendingStartObservingChildren) {
if (instance.isPendingStartObservingChildren) {
return;
}

instance.pendingStartObservingChildren = true;
instance.isPendingStartObservingChildren = true;
stopObservingChildren(instance);

requestAnimationFrame(() => {
instance.pendingStartObservingChildren = false;
instance.isPendingStartObservingChildren = false;
startObservingChildren(instance);
});
}
Expand Down Expand Up @@ -53,15 +53,15 @@ function startObservingContainer(instance: IRectpackr) {
}

function startObservingImages(instance: IRectpackr) {
function callback(this: HTMLImageElement) {
function onImgLoad(this: HTMLImageElement) {
instance.loadingImages.delete(this);
restartObservingChildren(instance);
}

for (const img of instance.childrenContainer.querySelectorAll('img')) {
if (!img.complete && !instance.loadingImages.get(img)) {
instance.loadingImages.set(img, callback);
img.addEventListener('load', callback, { once: true, passive: true });
instance.loadingImages.set(img, onImgLoad);
img.addEventListener('load', onImgLoad, { once: true, passive: true });
}
}
}
Expand All @@ -80,8 +80,8 @@ function stopObservingContainer(instance: IRectpackr) {
}

function stopObservingImages(instance: IRectpackr) {
for (const [img, callback] of instance.loadingImages) {
img.removeEventListener('load', callback);
for (const [img, onImgLoad] of instance.loadingImages) {
img.removeEventListener('load', onImgLoad);
instance.loadingImages.delete(img);
}
}
Expand Down Expand Up @@ -112,9 +112,7 @@ function updateStyle(
instance: IRectpackr,
children: { element: IRectpackrChildElement; point: [number, number] }[]
) {
/*
* Update children style.
*/
// Update children style
for (const { element, point } of children) {
const xVal =
point[0] *
Expand Down Expand Up @@ -143,9 +141,7 @@ function updateStyle(
}
}

/*
* Update container style.
*/
// Update container style
instance.container.style.height = `${instance.stripPack.packedHeight}px`;
}

Expand All @@ -154,6 +150,10 @@ function updateStyle(
/* ------------------------------------------------------------------------- */

export function onChildrenContainerMutation(instance: IRectpackr) {
if (instance.childrenContainer.children.length === 0) {
onChildResize(instance, []);
}

restartObservingChildren(instance);
restartObservingImages(instance);
}
Expand Down Expand Up @@ -196,9 +196,7 @@ export function onContainerResize(instance: IRectpackr) {
}

export function resetStyle(instance: IRectpackr) {
/*
* Reset children style.
*/
// Reset children style
for (const { element } of instance.children) {
if (instance.config.positioning === 'offset') {
element.style.inset = '';
Expand All @@ -207,9 +205,7 @@ export function resetStyle(instance: IRectpackr) {
}
}

/*
* Reset container style.
*/
// Reset container style
instance.container.style.height = '';
}

Expand Down
16 changes: 6 additions & 10 deletions tests/rectpackr-layout-attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@ import {
} from './util/types';

describe('Change attributes', () => {
const shadowRootOffsetChildrenStyle = Object.freeze({
ltr: '::slotted(:not([slot])){ position: absolute !important }',
rtl: '::slotted(:not([slot])){ position: absolute !important }',
ttb: '::slotted(:not([slot])){ position: absolute !important }',
btt: '::slotted(:not([slot])){ position: absolute !important }',
});
const shadowRootOffsetChildrenStyle =
'::slotted(:not([slot])){ position: absolute !important }';

const shadowRootTransformChildrenStyle = Object.freeze({
ltr: '::slotted(:not([slot])){ position: absolute !important; inset: 0 auto auto 0 !important }',
Expand Down Expand Up @@ -102,7 +98,7 @@ describe('Change attributes', () => {

changeAttributeAndValidate(
{ positioning: 'offset' },
shadowRootOffsetChildrenStyle.rtl,
shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: '0 0 auto auto' },
{ element: children[1]!, inset: `0 ${1 * childWidth}px auto auto` },
Expand All @@ -113,7 +109,7 @@ describe('Change attributes', () => {

changeAttributeAndValidate(
{ 'x-direction': 'ltr' },
shadowRootOffsetChildrenStyle.ltr,
shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: '0 auto auto 0' },
{ element: children[1]!, inset: `0 auto auto ${1 * childWidth}px` },
Expand Down Expand Up @@ -223,7 +219,7 @@ describe('Change attributes', () => {

changeAttributeAndValidate(
{ positioning: 'offset' },
shadowRootOffsetChildrenStyle.btt,
shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: 'auto auto 0 0' },
{ element: children[1]!, inset: `auto auto ${1 * childHeight}px 0` },
Expand All @@ -234,7 +230,7 @@ describe('Change attributes', () => {

changeAttributeAndValidate(
{ 'y-direction': 'ttb' },
shadowRootOffsetChildrenStyle.ttb,
shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: '0 auto auto 0' },
{ element: children[1]!, inset: `${1 * childHeight}px auto auto 0` },
Expand Down
Loading