aspect-grid-collageify 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/README_EN.md +246 -0
- package/dist/index.d.mts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.global.js +794 -0
- package/dist/index.js +796 -0
- package/dist/index.mjs +771 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 liuxin2533
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# aspect-grid-collageify
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/aspect-grid-collageify">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/aspect-grid-collageify.svg?style=flat-square&color=6366f1" alt="npm version">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://bundlephobia.com/package/aspect-grid-collageify">
|
|
8
|
+
<img src="https://img.shields.io/bundlephobia/min/aspect-grid-collageify?style=flat-square&color=indigo" alt="bundle size">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/aspect-grid-collageify">
|
|
11
|
+
<img src="https://img.shields.io/npm/dm/aspect-grid-collageify.svg?style=flat-square&color=pink" alt="downloads">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="file:///d:/Documents/Superme/npm/aspect-collageify/LICENSE">
|
|
14
|
+
<img src="https://img.shields.io/npm/l/aspect-grid-collageify.svg?style=flat-square&color=emerald" alt="license">
|
|
15
|
+
</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
一款轻量级、高性能的纯前端 HTML5 Canvas 等比智能网格拼图引擎。支持可视化交互编辑与后台离屏无损渲染,所有图片在排版中均严格保持比例锁定。
|
|
19
|
+
|
|
20
|
+
[English](./README_EN.md) | 简体中文
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ 特性
|
|
25
|
+
|
|
26
|
+
- 📐 **比例锁定**: 严格维持容器宽高比与单个图片格子比例,拉伸和缩放时绝不产生变形。
|
|
27
|
+
- 🎮 **可视化编辑器**: 支持拖拽自动吸附排版、选中高亮、悬停占位提示与实时画布重绘。
|
|
28
|
+
- 🎨 **单图美化调节**: 支持对**每一张图片单独调节**圆角半径、阴影模糊、阴影偏移及透明度,并拥有优雅的全局配置回退机制。
|
|
29
|
+
- ⚙️ **离屏渲染器**: 在后台静默渲染导出无辅助线和编辑高亮的超高分辨率 PNG 图像,完全脱离 DOM。
|
|
30
|
+
- 🛡️ **防碰撞与越界保护**: 内置几何数学网格算法,确保图片在缩放、移动时互不重叠且不越界。
|
|
31
|
+
- ↕️ **智能推拉排版**: 一键对目标图片下方的所有行进行整体下推或上拉,自动重整空白间隙。
|
|
32
|
+
- 📦 **零外部依赖**: 极小体积包(约 32KB IIFE),无任何第三方依赖,加载迅速。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 📦 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install aspect-grid-collageify
|
|
40
|
+
# 或
|
|
41
|
+
pnpm add aspect-grid-collageify
|
|
42
|
+
# 或
|
|
43
|
+
yarn add aspect-grid-collageify
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ⚡ 快速上手
|
|
49
|
+
|
|
50
|
+
### 1. 离屏无痕渲染(Headless 模式)
|
|
51
|
+
|
|
52
|
+
在后台静默生成拼图的 PNG 图像:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { AspectGridCollageify } from "aspect-grid-collageify";
|
|
56
|
+
|
|
57
|
+
async function makeCollage() {
|
|
58
|
+
const engine = new AspectGridCollageify({
|
|
59
|
+
containerRatio: "3:4",
|
|
60
|
+
imageRatio: "16:9",
|
|
61
|
+
gridColumns: 8,
|
|
62
|
+
padding2K: 60,
|
|
63
|
+
gap2K: 24,
|
|
64
|
+
containerBgColor: "#ffffff",
|
|
65
|
+
images: [
|
|
66
|
+
{ id: "img-1", src: "https://example.com/beach.jpg", name: "Beach", gridX: 0, gridY: 0, span: 4 },
|
|
67
|
+
{
|
|
68
|
+
id: "img-2",
|
|
69
|
+
src: "https://example.com/mountain.jpg",
|
|
70
|
+
name: "Mountain",
|
|
71
|
+
gridX: 4,
|
|
72
|
+
gridY: 0,
|
|
73
|
+
span: 4,
|
|
74
|
+
borderRadius2K: 48, // 仅单独覆盖此图片的圆角半径
|
|
75
|
+
shadowBlur2K: 30, // 仅单独为此图片定制阴影模糊值
|
|
76
|
+
},
|
|
77
|
+
{ id: "img-3", src: "https://example.com/forest.jpg", name: "Forest", gridX: 2, gridY: 4, span: 4 },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 导出 2K 高清 PNG Base64 (宽度: 2048px, 高度: 2730px)
|
|
82
|
+
const base64Png = await engine.exportPNG(2048);
|
|
83
|
+
console.log("生成的拼图 Base64 编码:", base64Png);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. 可视化编辑器(Visual 交互模式)
|
|
88
|
+
|
|
89
|
+
在原生 HTML 或前端框架组件中绑定一个 `<canvas>` 进行实时可视化编辑:
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<canvas id="collage-canvas" style="width: 100%; height: 100%;"></canvas>
|
|
93
|
+
|
|
94
|
+
<script type="module">
|
|
95
|
+
import { AspectGridCollageify } from 'aspect-grid-collageify';
|
|
96
|
+
|
|
97
|
+
const canvas = document.getElementById("collage-canvas");
|
|
98
|
+
const engine = new AspectGridCollageify({
|
|
99
|
+
containerRatio: "3:4",
|
|
100
|
+
imageRatio: "16:9",
|
|
101
|
+
gridColumns: 8,
|
|
102
|
+
padding2K: 60,
|
|
103
|
+
gap2K: 24,
|
|
104
|
+
imageBorderRadius2K: 24, // 全局默认圆角半径
|
|
105
|
+
images: []
|
|
106
|
+
}, canvas);
|
|
107
|
+
|
|
108
|
+
// 订阅图片数组变更
|
|
109
|
+
engine.onImagesChanged((images) => {
|
|
110
|
+
console.log("图片排版更新:", images);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 订阅选中状态变更
|
|
114
|
+
engine.onActiveImageChanged((activeId) => {
|
|
115
|
+
console.log("当前选中图片 ID:", activeId);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// 订阅空白网格点击事件
|
|
119
|
+
engine.onCellClicked((x, y) => {
|
|
120
|
+
console.log("点击了空白网格的坐标:", x, y);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 初始绘制
|
|
124
|
+
engine.render();
|
|
125
|
+
</script>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 📖 API 接口说明
|
|
131
|
+
|
|
132
|
+
### 1. 配置项选项 (`CollageConfig`)
|
|
133
|
+
|
|
134
|
+
在构造函数中传入,用于初始化拼图引擎状态:
|
|
135
|
+
|
|
136
|
+
| 配置属性 | 类型 | 默认值 | 描述 |
|
|
137
|
+
| :--- | :--- | :--- | :--- |
|
|
138
|
+
| `containerRatio` | `string` | `"3:4"` | 画布的整体宽高比:`"1:1"`, `"3:4"`, `"4:3"`, `"16:9"`, `"9:16"` 或 `"custom"`(自定义)。 |
|
|
139
|
+
| `customContainerW` | `number` | - | 自定义画布宽比例值(在 `containerRatio` 为 `"custom"` 时必填)。 |
|
|
140
|
+
| `customContainerH` | `number` | - | 自定义画布高比例值(在 `containerRatio` 为 `"custom"` 时必填)。 |
|
|
141
|
+
| `imageRatio` | `string` | `"16:9"` | 单张图片格子的默认宽高比:`"1:1"`, `"4:3"`, `"16:9"` 或 `"custom"`。 |
|
|
142
|
+
| `customImageW` | `number` | - | 自定义单图格子宽比例值(在 `imageRatio` 为 `"custom"` 时必填)。 |
|
|
143
|
+
| `customImageH` | `number` | - | 自定义单图格子高比例值(在 `imageRatio` 为 `"custom"` 时必填)。 |
|
|
144
|
+
| `gridColumns` | `number` | `8` | 网格列密度数(可设置范围为 `4` 至 `48` 之间的双数)。 |
|
|
145
|
+
| `padding2K` | `number` | `60` | 拼图外边缘内边距(以 2K 物理宽度为基准按比例缩放,单位像素)。 |
|
|
146
|
+
| `gap2K` | `number` | `24` | 拼图格子之间的间距(以 2K 物理宽度为基准按比例缩放,单位像素)。 |
|
|
147
|
+
| `containerBgColor` | `string` | `"#ffffff"` | 画布背景颜色。 |
|
|
148
|
+
| `useTransparentBg` | `boolean` | `false` | 是否启用透明通道。为 `true` 时,导出 PNG 会保留透明背景。 |
|
|
149
|
+
| `images` | `PlacedImage[]` | `[]` | 初始载入排版的图片块数组。 |
|
|
150
|
+
| `showGridlines` | `boolean` | `true` | 是否在可视化编辑模式下显示虚线辅助网格线。 |
|
|
151
|
+
| `placementSize` | `string` | `"medium"` | 默认的空白格大小:`"small"`, `"medium"`, `"large"`。 |
|
|
152
|
+
| `imageBorderRadius2K`| `number` | `24` | 全局的图片卡片圆角半径(以 2K 为基准缩放)。 |
|
|
153
|
+
| `imageShadowBlur2K` | `number` | `0` | 全局的图片卡片阴影模糊半径(以 2K 为基准缩放)。 |
|
|
154
|
+
| `imageShadowOffset2K`| `number` | `0` | 全局的图片卡片阴影位移大小(以 2K 为基准缩放)。 |
|
|
155
|
+
| `imageShadowOpacity` | `number` | `0.2` | 全局的图片卡片阴影不透明度(0 到 1)。 |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### 2. 已放置图片结构 (`PlacedImage`)
|
|
160
|
+
|
|
161
|
+
代表每一个已放置到网格中的图片卡片节点对象:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
export interface PlacedImage {
|
|
165
|
+
id: string; // 节点的唯一 ID
|
|
166
|
+
src: string; // 图片的访问链接/Base64/ObjectURL
|
|
167
|
+
name: string; // 文件名/描述标签
|
|
168
|
+
gridX: number; // 网格列起始索引(从 0 开始)
|
|
169
|
+
gridY: number; // 网格行起始索引(从 0 开始)
|
|
170
|
+
span: number; // 图片块在网格中所占用的横跨大小(正方形 span * span)
|
|
171
|
+
|
|
172
|
+
// 可选:单张图片个体的外观配置覆写(覆盖全局 defaults)
|
|
173
|
+
borderRadius2K?: number; // 覆写全局的 imageBorderRadius2K 圆角设置
|
|
174
|
+
shadowBlur2K?: number; // 覆写全局的 imageShadowBlur2K 阴影模糊设置
|
|
175
|
+
shadowOffset2K?: number; // 覆写全局的 imageShadowOffset2K 阴影偏移设置
|
|
176
|
+
shadowOpacity?: number; // 覆写全局的 imageShadowOpacity 阴影透明度设置
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### 3. 类方法 (`AspectGridCollageify`)
|
|
183
|
+
|
|
184
|
+
#### 🎨 视觉渲染与配置更新
|
|
185
|
+
|
|
186
|
+
- **`render(drawUI: boolean = true)`**: 触发 Canvas 画布渲染绘制。传入 `false` 时将渲染不带任何辅助虚线、选中边框的干净视图。
|
|
187
|
+
|
|
188
|
+
- **`updateConfig(config: Partial<CollageConfig>)`**: 动态更新配置参数,并自动重绘画布。
|
|
189
|
+
- **`getConfig(): CollageConfig`**: 获取当前生效的全部配置参数。
|
|
190
|
+
|
|
191
|
+
#### 📂 图片管理 API
|
|
192
|
+
|
|
193
|
+
- **`getImages(): PlacedImage[]`**: 获取当前画布中放置的所有图片节点数组。
|
|
194
|
+
|
|
195
|
+
- **`setImages(images: PlacedImage[])`**: 替换画布中的所有图片,并自动触发计算排版。
|
|
196
|
+
- **`addImage(img: PlacedImage)`**: 添加一张图片至拼图网格并默认激活选中它。
|
|
197
|
+
- **`removeImage(imgId: string)`**: 通过 ID 移出某张已放置的图片。
|
|
198
|
+
- **`updateImage(imgId: string, updates: Partial<PlacedImage>)`**: 动态更新目标图片的专属属性(如位置坐标、格跨大小,或单独覆写其圆角/阴影等样式),并立即调度 Canvas 画布重绘。
|
|
199
|
+
|
|
200
|
+
#### 🕹️ 坐标微调与排列对齐
|
|
201
|
+
|
|
202
|
+
- **`modifyImageSpan(imgId: string, delta: number, gridRows: number): boolean`**: 缩放目标图片的网格占比大小 (+1/-1)。如果计算防撞和出界检测成功则返回 `true`。
|
|
203
|
+
|
|
204
|
+
- **`stepImagePosition(imgId: string, dir: "up" | "down" | "left" | "right", gridRows: number): boolean`**: 使目标图片在网格坐标系中向指定方向平移 1 个单位。
|
|
205
|
+
- **`pushDownBelow(imgId: string, gridRows: number): boolean`**: 将当前选定图片底边以下的所有其他图片卡片,整体下推 1 个网格单位。
|
|
206
|
+
- **`pullUpBelow(imgId: string): boolean`**: 如果空间允许,将当前选定图片底边以下的所有图片卡片,整体上拉 1 个网格单位以消除空白行隙。
|
|
207
|
+
|
|
208
|
+
#### ⚡ 交互事件订阅
|
|
209
|
+
|
|
210
|
+
- **`onImagesChanged(callback: (images: PlacedImage[]) => void)`**: 当画布内图片发生增加、删除、位移、缩放或美化属性调整时触发。
|
|
211
|
+
|
|
212
|
+
- **`onActiveImageChanged(callback: (id: string | null) => void)`**: 当选中的图片节点发生改变时触发(传入 null 代表取消选中)。
|
|
213
|
+
- **`onCellClicked(callback: (x: number, y: number) => void)`**: 当点击空白辅助格子时触发,返回点击的网格坐标 $(X, Y)$。
|
|
214
|
+
|
|
215
|
+
#### 💾 离屏无损导出
|
|
216
|
+
|
|
217
|
+
- **`exportPNG(targetWidth: number = 2048): Promise<string>`**: 异步预加载所有拼图图片,自动创建一个独立的超高分辨率后台离屏 Canvas,绘制无辅助线的洁净画面,并 resolve 返回 PNG DataURL (Base64) 字符串。
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 📐 几何网格对齐与缩放数学
|
|
222
|
+
|
|
223
|
+
拼图的位置与尺寸转换由网格几何数学公式严格驱动,将离散的网格坐标转换为实际物理像素点:
|
|
224
|
+
|
|
225
|
+
1. **基准缩放比计算**:
|
|
226
|
+
$$\text{scale} = \frac{\text{width}}{2048}$$
|
|
227
|
+
该公式保证了在 2K 物理宽度基准下定义的 `padding`, `gap`, `radius`, `shadow` 等参数,在导出 4K 高清大图时能够得到同比的高清拉伸。
|
|
228
|
+
2. **格子物理像素尺寸换算**:
|
|
229
|
+
$$\text{cellW} = \frac{\text{width} - 2 \cdot \text{padding} - (\text{gridColumns} - 1) \cdot \text{gap}}{\text{gridColumns}}$$
|
|
230
|
+
$$\text{cellH} = \frac{\text{cellW} + \text{gap}}{\text{imageRatioVal}} - \text{gap}$$
|
|
231
|
+
3. **碰撞相交判断方程**:
|
|
232
|
+
$$\text{Collision} = \neg (X_{1} + S_{1} \le X_{2} \lor X_{2} + S_{2} \le X_{1} \lor Y_{1} + S_{1} \le Y_{2} \lor Y_{2} + S_{2} \le Y_{1})$$
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 🛠️ 本地运行与开发调试
|
|
237
|
+
|
|
238
|
+
1. **编译打包构建库文件**:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pnpm install
|
|
242
|
+
pnpm build
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
2. **启动本地开发调试服务器 (Vite Dev Server)**:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
pnpm dev
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
在浏览器中打开 `http://localhost:5173/`。修改 `./src/index.ts` 将会立即热重载刷新浏览器内的画布效果。
|
|
252
|
+
3. **静态文件本地预览 (免跨域 Offline 模式)**:
|
|
253
|
+
你也可以直接双击 `index.html` 离线运行。双协议加载器会自动判定 `file://` 协议并动态注入打包好的 `./dist/index.global.js` UMD 库文件,无需本地 HTTP 代理即可预览调试全部功能,防止浏览器产生文件跨域拦截报错。
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 📄 开源许可证
|
|
258
|
+
|
|
259
|
+
MIT
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# aspect-grid-collageify
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/aspect-grid-collageify">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/aspect-grid-collageify.svg?style=flat-square&color=6366f1" alt="npm version">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://bundlephobia.com/package/aspect-grid-collageify">
|
|
8
|
+
<img src="https://img.shields.io/bundlephobia/min/aspect-grid-collageify?style=flat-square&color=indigo" alt="bundle size">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/aspect-grid-collageify">
|
|
11
|
+
<img src="https://img.shields.io/npm/dm/aspect-grid-collageify.svg?style=flat-square&color=pink" alt="downloads">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="file:///d:/Documents/Superme/npm/aspect-collageify/LICENSE">
|
|
14
|
+
<img src="https://img.shields.io/npm/l/aspect-grid-collageify.svg?style=flat-square&color=emerald" alt="license">
|
|
15
|
+
</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
A lightweight, high-performance, pure-frontend HTML5 Canvas-based smart photo collage engine supporting visual interactive editing and headless offscreen rendering. All images maintain their locked aspect ratios dynamically.
|
|
19
|
+
|
|
20
|
+
English | [简体中文](./README.md)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ Features
|
|
25
|
+
|
|
26
|
+
- 📐 **Proportional Locking**: Rigid adherence to container aspect ratios and individual image aspect ratios during placement.
|
|
27
|
+
- 🎮 **Interactive Visual Editor**: Drag-and-drop snapping layout, selection highlighting, hover slot indicators, and live redrawing.
|
|
28
|
+
- 🎨 **Per-Image Visual Aesthetics**: Customize border radius, drop shadow blur, offsets, and opacity **individually per image**, with a clean rollback structure to global configurations.
|
|
29
|
+
- ⚙️ **Offscreen Headless Renderer**: Generate clean, high-resolution PNGs (Base64) silently in the background using identical layout algorithms, completely independent of the DOM.
|
|
30
|
+
- 🛡️ **Anti-Overlap Safeguards**: Built-in geometric grid math guarantees that images cannot overlap or exceed boundaries during movement or resizing.
|
|
31
|
+
- ↕️ **Smart Row Shifting**: Shift all rows below a specific image node downward or pull them up to reclaim empty vertical gaps in a single operation.
|
|
32
|
+
- 📦 **Zero Dependencies**: Ultra-compact bundled footprint (~32KB IIFE), extremely fast loading.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 📦 Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install aspect-grid-collageify
|
|
40
|
+
# or
|
|
41
|
+
pnpm add aspect-grid-collageify
|
|
42
|
+
# or
|
|
43
|
+
yarn add aspect-grid-collageify
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ⚡ Quick Start
|
|
49
|
+
|
|
50
|
+
### 1. Offscreen Rendering (Headless Mode)
|
|
51
|
+
|
|
52
|
+
Generate a collage PNG silently in the background:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { AspectGridCollageify } from "aspect-grid-collageify";
|
|
56
|
+
|
|
57
|
+
async function makeCollage() {
|
|
58
|
+
const engine = new AspectGridCollageify({
|
|
59
|
+
containerRatio: "3:4",
|
|
60
|
+
imageRatio: "16:9",
|
|
61
|
+
gridColumns: 8,
|
|
62
|
+
padding2K: 60,
|
|
63
|
+
gap2K: 24,
|
|
64
|
+
containerBgColor: "#ffffff",
|
|
65
|
+
images: [
|
|
66
|
+
{ id: "img-1", src: "https://example.com/beach.jpg", name: "Beach", gridX: 0, gridY: 0, span: 4 },
|
|
67
|
+
{
|
|
68
|
+
id: "img-2",
|
|
69
|
+
src: "https://example.com/mountain.jpg",
|
|
70
|
+
name: "Mountain",
|
|
71
|
+
gridX: 4,
|
|
72
|
+
gridY: 0,
|
|
73
|
+
span: 4,
|
|
74
|
+
borderRadius2K: 48, // Override global radius for this image
|
|
75
|
+
shadowBlur2K: 30, // Customize drop shadow for this image
|
|
76
|
+
},
|
|
77
|
+
{ id: "img-3", src: "https://example.com/forest.jpg", name: "Forest", gridX: 2, gridY: 4, span: 4 },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Export to 2K High-Res PNG Base64 (width: 2048px, height: 2730px)
|
|
82
|
+
const base64Png = await engine.exportPNG(2048);
|
|
83
|
+
console.log("Clean output Image Base64:", base64Png);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. Interactive Editor (Visual Mode)
|
|
88
|
+
|
|
89
|
+
Bind an interactive canvas in vanilla HTML or framework components:
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<canvas id="collage-canvas" style="width: 100%; height: 100%;"></canvas>
|
|
93
|
+
|
|
94
|
+
<script type="module">
|
|
95
|
+
import { AspectGridCollageify } from 'aspect-grid-collageify';
|
|
96
|
+
|
|
97
|
+
const canvas = document.getElementById("collage-canvas");
|
|
98
|
+
const engine = new AspectGridCollageify({
|
|
99
|
+
containerRatio: "3:4",
|
|
100
|
+
imageRatio: "16:9",
|
|
101
|
+
gridColumns: 8,
|
|
102
|
+
padding2K: 60,
|
|
103
|
+
gap2K: 24,
|
|
104
|
+
imageBorderRadius2K: 24, // Global default radius
|
|
105
|
+
images: []
|
|
106
|
+
}, canvas);
|
|
107
|
+
|
|
108
|
+
// Subscribe to updates
|
|
109
|
+
engine.onImagesChanged((images) => {
|
|
110
|
+
console.log("Images array updated:", images);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
engine.onActiveImageChanged((activeId) => {
|
|
114
|
+
console.log("Selected image changed to:", activeId);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
engine.onCellClicked((x, y) => {
|
|
118
|
+
console.log("Clicked empty slot coordinates:", x, y);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Initial draw
|
|
122
|
+
engine.render();
|
|
123
|
+
</script>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 📖 API References
|
|
129
|
+
|
|
130
|
+
### 1. Configuration Options (`CollageConfig`)
|
|
131
|
+
|
|
132
|
+
Passed to the constructor to initialize the engine state:
|
|
133
|
+
|
|
134
|
+
| Attribute | Type | Default | Description |
|
|
135
|
+
| :--- | :--- | :--- | :--- |
|
|
136
|
+
| `containerRatio` | `string` | `"3:4"` | Canvas aspect ratio: `"1:1"`, `"3:4"`, `"4:3"`, `"16:9"`, `"9:16"`, or `"custom"`. |
|
|
137
|
+
| `customContainerW` | `number` | - | Width ratio value (required if `containerRatio` is `"custom"`). |
|
|
138
|
+
| `customContainerH` | `number` | - | Height ratio value (required if `containerRatio` is `"custom"`). |
|
|
139
|
+
| `imageRatio` | `string` | `"16:9"` | Aspect ratio of grid item: `"1:1"`, `"4:3"`, `"16:9"`, or `"custom"`. |
|
|
140
|
+
| `customImageW` | `number` | - | Image width ratio value (required if `imageRatio` is `"custom"`). |
|
|
141
|
+
| `customImageH` | `number` | - | Image height ratio value (required if `imageRatio` is `"custom"`). |
|
|
142
|
+
| `gridColumns` | `number` | `8` | Column partition density (ranges from `4` to `48`). |
|
|
143
|
+
| `padding2K` | `number` | `60` | Edge border padding in pixels (scaled relative to 2K width). |
|
|
144
|
+
| `gap2K` | `number` | `24` | Inner spacing gap between items in pixels (scaled relative to 2K). |
|
|
145
|
+
| `containerBgColor` | `string` | `"#ffffff"` | Solid background color for the canvas layout. |
|
|
146
|
+
| `useTransparentBg` | `boolean` | `false` | If true, clear color channel for transparent alpha exports. |
|
|
147
|
+
| `images` | `PlacedImage[]` | `[]` | Initial photo elements loaded in the layout. |
|
|
148
|
+
| `showGridlines` | `boolean` | `true` | Show helper dashed gridlines in interactive edit mode. |
|
|
149
|
+
| `placementSize` | `string` | `"medium"` | Default span size for empty slots: `"small"`, `"medium"`, `"large"`. |
|
|
150
|
+
| `imageBorderRadius2K`| `number` | `24` | Global fallback image corner radius (scaled to 2K width). |
|
|
151
|
+
| `imageShadowBlur2K` | `number` | `0` | Global fallback drop shadow blur size (scaled to 2K width). |
|
|
152
|
+
| `imageShadowOffset2K`| `number` | `0` | Global fallback drop shadow directional offset (scaled to 2K). |
|
|
153
|
+
| `imageShadowOpacity` | `number` | `0.2` | Global fallback drop shadow opacity value (0 to 1). |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### 2. Placed Image Structure (`PlacedImage`)
|
|
158
|
+
|
|
159
|
+
Represents each image block placed on the canvas:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
export interface PlacedImage {
|
|
163
|
+
id: string; // Unique block ID
|
|
164
|
+
src: string; // Photo URL / DataURI / ObjectURL
|
|
165
|
+
name: string; // Display filename/label
|
|
166
|
+
gridX: number; // Column starting index (0-based)
|
|
167
|
+
gridY: number; // Row starting index (0-based)
|
|
168
|
+
span: number; // Width/height block scale span in grid coordinates
|
|
169
|
+
|
|
170
|
+
// Optional Individual Aesthetics overrides
|
|
171
|
+
borderRadius2K?: number; // Overrides global imageBorderRadius2K
|
|
172
|
+
shadowBlur2K?: number; // Overrides global imageShadowBlur2K
|
|
173
|
+
shadowOffset2K?: number; // Overrides global imageShadowOffset2K
|
|
174
|
+
shadowOpacity?: number; // Overrides global imageShadowOpacity
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### 3. Class Methods (`AspectGridCollageify`)
|
|
181
|
+
|
|
182
|
+
#### 🎨 Visual Render & Config
|
|
183
|
+
* **`render(drawUI: boolean = true)`**: Trigger a render pass on the interactive canvas. Pass `false` to render clean content without helpers.
|
|
184
|
+
* **`updateConfig(config: Partial<CollageConfig>)`**: Dynamically modify config options and trigger auto-redraw.
|
|
185
|
+
* **`getConfig(): CollageConfig`**: Get the current active configuration.
|
|
186
|
+
|
|
187
|
+
#### 📂 Image Node Management
|
|
188
|
+
* **`getImages(): PlacedImage[]`**: Returns the list of placed images.
|
|
189
|
+
* **`setImages(images: PlacedImage[])`**: Replaces all images and triggers layout updates.
|
|
190
|
+
* **`addImage(img: PlacedImage)`**: Add a new image node to the collage and select it.
|
|
191
|
+
* **`removeImage(imgId: string)`**: Delete an image node by ID.
|
|
192
|
+
* **`updateImage(imgId: string, updates: Partial<PlacedImage>)`**: Updates specific image configuration fields (e.g. coordinates, span, or per-image radius/shadow overrides) and schedules an immediate canvas repaint.
|
|
193
|
+
|
|
194
|
+
#### 🕹️ Micro-Adjustments & Alignment
|
|
195
|
+
* **`modifyImageSpan(imgId: string, delta: number, gridRows: number): boolean`**: Scale target image span (+1/-1). Returns `true` if operation was successful (bounds and collisions checked).
|
|
196
|
+
* **`stepImagePosition(imgId: string, dir: "up" | "down" | "left" | "right", gridRows: number): boolean`**: Step coordinate position by 1 cell in grid coordinates.
|
|
197
|
+
* **`pushDownBelow(imgId: string, gridRows: number): boolean`**: Shift all images below the target block's bottom border downwards by 1 row.
|
|
198
|
+
* **`pullUpBelow(imgId: string): boolean`**: Pull up all images below the target block's bottom border upwards by 1 row if empty space permits.
|
|
199
|
+
|
|
200
|
+
#### ⚡ Event Subscriptions
|
|
201
|
+
* **`onImagesChanged(callback: (images: PlacedImage[]) => void)`**: Triggered when any image is added, moved, scaled, updated, or deleted.
|
|
202
|
+
* **`onActiveImageChanged(callback: (id: string | null) => void)`**: Triggered when a grid node selection changes.
|
|
203
|
+
* **`onCellClicked(callback: (x: number, y: number) => void)`**: Triggered when clicking empty helper grid placeholder slots.
|
|
204
|
+
|
|
205
|
+
#### 💾 Exporter
|
|
206
|
+
* **`exportPNG(targetWidth: number = 2048): Promise<string>`**: Asynchronously preloads all grid images, constructs a high-resolution headless Canvas, renders clean output, and resolves to a PNG DataURL.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 📐 Geometric Alignment & Rendering Math
|
|
211
|
+
|
|
212
|
+
The collage layouts are calculated dynamically. The grid formulas convert grid coordinates into logical pixels:
|
|
213
|
+
|
|
214
|
+
1. **Resolution Scale Calculation**:
|
|
215
|
+
$$\text{scale} = \frac{\text{width}}{2048}$$
|
|
216
|
+
This ensures visual sizing metrics defined as "2K" (padding, gap, radius, shadow) scale proportionally at higher resolutions.
|
|
217
|
+
2. **Cell Geometric Formulations**:
|
|
218
|
+
$$\text{cellW} = \frac{\text{width} - 2 \cdot \text{padding} - (\text{gridColumns} - 1) \cdot \text{gap}}{\text{gridColumns}}$$
|
|
219
|
+
$$\text{cellH} = \frac{\text{cellW} + \text{gap}}{\text{imageRatioVal}} - \text{gap}$$
|
|
220
|
+
3. **Collision Checks**:
|
|
221
|
+
$$\text{Collision} = \neg (X_{1} + S_{1} \le X_{2} \lor X_{2} + S_{2} \le X_{1} \lor Y_{1} + S_{1} \le Y_{2} \lor Y_{2} + S_{2} \le Y_{1})$$
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 🛠️ Run The Interactive Demo
|
|
226
|
+
|
|
227
|
+
To test the library locally inside a browser:
|
|
228
|
+
|
|
229
|
+
1. **Build the packages**:
|
|
230
|
+
```bash
|
|
231
|
+
pnpm install
|
|
232
|
+
pnpm build
|
|
233
|
+
```
|
|
234
|
+
2. **Launch Developer Dev Server (Hot Module Reloading)**:
|
|
235
|
+
```bash
|
|
236
|
+
pnpm dev
|
|
237
|
+
```
|
|
238
|
+
Open `http://localhost:5173/` in a browser. Any updates to `./src/index.ts` will trigger instant updates in the browser canvas.
|
|
239
|
+
3. **Static File Testing (CORS-free Offline mode)**:
|
|
240
|
+
You can also double-click `index.html` to open it in a browser directly via `file://`. The dual-protocol loader automatically loads the precompiled `./dist/index.global.js` UMD bundle, preventing local filesystem CORS blocks.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
interface PlacedImage {
|
|
2
|
+
id: string;
|
|
3
|
+
src: string;
|
|
4
|
+
name: string;
|
|
5
|
+
gridX: number;
|
|
6
|
+
gridY: number;
|
|
7
|
+
span: number;
|
|
8
|
+
borderRadius2K?: number;
|
|
9
|
+
shadowBlur2K?: number;
|
|
10
|
+
shadowOffset2K?: number;
|
|
11
|
+
shadowOpacity?: number;
|
|
12
|
+
}
|
|
13
|
+
interface CollageConfig {
|
|
14
|
+
containerRatio: string;
|
|
15
|
+
customContainerW?: number;
|
|
16
|
+
customContainerH?: number;
|
|
17
|
+
imageRatio: string;
|
|
18
|
+
customImageW?: number;
|
|
19
|
+
customImageH?: number;
|
|
20
|
+
gridColumns: number;
|
|
21
|
+
padding2K: number;
|
|
22
|
+
gap2K: number;
|
|
23
|
+
containerBgColor?: string;
|
|
24
|
+
useTransparentBg?: boolean;
|
|
25
|
+
images: PlacedImage[];
|
|
26
|
+
showGridlines?: boolean;
|
|
27
|
+
placementSize?: "small" | "medium" | "large";
|
|
28
|
+
imageBorderRadius2K?: number;
|
|
29
|
+
imageShadowBlur2K?: number;
|
|
30
|
+
imageShadowOffset2K?: number;
|
|
31
|
+
imageShadowOpacity?: number;
|
|
32
|
+
}
|
|
33
|
+
declare class AspectGridCollageify {
|
|
34
|
+
private config;
|
|
35
|
+
private canvas;
|
|
36
|
+
private ctx;
|
|
37
|
+
private imageCache;
|
|
38
|
+
private loadingImages;
|
|
39
|
+
private activeImageId;
|
|
40
|
+
private draggedImageId;
|
|
41
|
+
private dragStartOffset;
|
|
42
|
+
private dragCurrentPos;
|
|
43
|
+
private draggedOverCell;
|
|
44
|
+
private hoveredPlaceholder;
|
|
45
|
+
private isDragging;
|
|
46
|
+
private changeCallbacks;
|
|
47
|
+
private activeCallbacks;
|
|
48
|
+
private clickCallbacks;
|
|
49
|
+
constructor(config: CollageConfig, canvasElement?: HTMLCanvasElement);
|
|
50
|
+
getConfig(): CollageConfig;
|
|
51
|
+
updateConfig(newConfig: Partial<CollageConfig>): void;
|
|
52
|
+
getImages(): PlacedImage[];
|
|
53
|
+
setImages(images: PlacedImage[]): void;
|
|
54
|
+
getActiveImageId(): string | null;
|
|
55
|
+
setActiveImageId(id: string | null): void;
|
|
56
|
+
onImagesChanged(callback: (images: PlacedImage[]) => void): void;
|
|
57
|
+
onActiveImageChanged(callback: (id: string | null) => void): void;
|
|
58
|
+
onCellClicked(callback: (x: number, y: number) => void): void;
|
|
59
|
+
private triggerChange;
|
|
60
|
+
private triggerActiveChange;
|
|
61
|
+
private triggerCellClick;
|
|
62
|
+
private getOrLoadImage;
|
|
63
|
+
private preloadAllImages;
|
|
64
|
+
canPlaceImage(imgId: string | null, x: number, y: number, spanSize: number, gridRows: number): boolean;
|
|
65
|
+
addImage(img: PlacedImage): void;
|
|
66
|
+
removeImage(imgId: string): void;
|
|
67
|
+
updateImage(imgId: string, updates: Partial<PlacedImage>): void;
|
|
68
|
+
modifyImageSpan(imgId: string, delta: number, gridRows: number): boolean;
|
|
69
|
+
stepImagePosition(imgId: string, dir: "up" | "down" | "left" | "right", gridRows: number): boolean;
|
|
70
|
+
pushDownBelow(imgId: string, gridRows: number): boolean;
|
|
71
|
+
pullUpBelow(imgId: string): boolean;
|
|
72
|
+
calculateLayout(width: number, height: number): {
|
|
73
|
+
scale: number;
|
|
74
|
+
padding: number;
|
|
75
|
+
gap: number;
|
|
76
|
+
cellW: number;
|
|
77
|
+
cellH: number;
|
|
78
|
+
gridRows: number;
|
|
79
|
+
offsetX: number;
|
|
80
|
+
offsetY: number;
|
|
81
|
+
gridW: number;
|
|
82
|
+
gridH: number;
|
|
83
|
+
containerRatioVal: number;
|
|
84
|
+
imageRatioVal: number;
|
|
85
|
+
};
|
|
86
|
+
private initEvents;
|
|
87
|
+
private hitTest;
|
|
88
|
+
private getImageRect;
|
|
89
|
+
private getCurrentTargetSpan;
|
|
90
|
+
getPlaceholders(targetSpan: number, gridRows: number): Array<{
|
|
91
|
+
gridX: number;
|
|
92
|
+
gridY: number;
|
|
93
|
+
span: number;
|
|
94
|
+
}>;
|
|
95
|
+
render(drawUI?: boolean): void;
|
|
96
|
+
/**
|
|
97
|
+
* The master draw function: compiles background, images, and overlays.
|
|
98
|
+
* Can be shared between interactive Canvas on the page and high-res offscreen Canvas.
|
|
99
|
+
*/
|
|
100
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number, drawUI: boolean): void;
|
|
101
|
+
exportPNG(targetWidth?: number): Promise<string>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { AspectGridCollageify, type CollageConfig, type PlacedImage };
|