image-grid-kit 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cutmyimage.com
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,133 @@
1
+ # image-grid-kit
2
+
3
+ Zero-dependency helpers for two jobs that keep showing up when you work with
4
+ sprite sheets and tiled images:
5
+
6
+ 1. **Splitting** — work out the exact pixel rectangles when an image is cut
7
+ into rows, columns or a grid, including the case where the pieces have to
8
+ come out at a fixed aspect ratio.
9
+ 2. **Playback** — turn those rectangles into CSS: one class per frame plus a
10
+ `@keyframes` block that steps through the sheet.
11
+
12
+ No build step, no dependencies, Node 18+.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install image-grid-kit
18
+ ```
19
+
20
+ or use it straight from a clone:
21
+
22
+ ```bash
23
+ node bin/cli.js grid --width 1600 --height 900 --cols 3 --rows 1
24
+ ```
25
+
26
+ ## Library
27
+
28
+ ```js
29
+ import { planGrid, planAspect, spriteCss } from "image-grid-kit";
30
+
31
+ const { pieces } = planGrid({ width: 1600, height: 900, cols: 3, rows: 1 });
32
+ // -> [{ n:1, x:0, y:0, w:534, h:900 },
33
+ // { n:2, x:534, y:0, w:533, h:900 },
34
+ // { n:3, x:1067, y:0, w:533, h:900 }]
35
+
36
+ console.log(spriteCss({ pieces, sheetUrl: "walk.png", prefix: "walk" }));
37
+ ```
38
+
39
+ ### `planGrid({ width, height, mode, sizing, cols, rows, tileW, tileH })`
40
+
41
+ `mode` is `grid`, `columns` or `rows`. `sizing` is `count` (number of pieces)
42
+ or `pixel` (fixed cell size, with an optional `--dropPartial` to throw the
43
+ short remainder away).
44
+
45
+ When the width does not divide evenly, the first `width % cols` columns get one
46
+ extra pixel, so the pieces always add back up to the original width:
47
+
48
+ ```js
49
+ planGrid({ width: 1600, height: 900, cols: 3, rows: 1 }).xs; // [534, 533, 533]
50
+ ```
51
+
52
+ ### `planAspect({ width, height, cols, rows, ratioW, ratioH })`
53
+
54
+ The largest centre crop whose shape makes every cell of a `cols x rows` grid
55
+ come out at exactly `ratioW : ratioH`. For a 3-piece grid at 4:5 on a 1600x900
56
+ image:
57
+
58
+ ```js
59
+ planAspect({ width: 1600, height: 900, cols: 3, rows: 1, ratioW: 4, ratioH: 5 });
60
+ // { crop: { x: 2, y: 117, w: 1596, h: 665 },
61
+ // cut: { left: 2, right: 2, top: 117, bottom: 118 },
62
+ // pieceW: 532, pieceH: 665 }
63
+ ```
64
+
65
+ Returns `null` when the image is too small to hold even one cell at that ratio.
66
+
67
+ ### `spriteCss({ pieces, sheetUrl, prefix, duration, timing, iterations })`
68
+
69
+ Emits a positioned class per frame, a `@keyframes` block, and an animation
70
+ class. `timing` defaults to `step-end`, which is what makes a sprite sheet play
71
+ frame by frame instead of sliding between frames:
72
+
73
+ ```css
74
+ @keyframes walk-play {
75
+ 0% { background-position: -0px -0px; }
76
+ 25% { background-position: -48px -0px; }
77
+ 50% { background-position: -96px -0px; }
78
+ 75% { background-position: -144px -0px; }
79
+ 100% { background-position: -144px -0px; }
80
+ }
81
+ .walk-anim { animation: walk-play 800ms step-end infinite; }
82
+ ```
83
+
84
+ ## CLI
85
+
86
+ ```
87
+ grid --width N --height N [--mode grid|columns|rows] [--cols N --rows N]
88
+ [--sizing count|pixel --tileW N --tileH N] [--dropPartial] [--json]
89
+ aspect --width N --height N --cols N --rows N [--ratio 4:5] [--json]
90
+ css --width N --height N --cols N --rows N [--sheet FILE] [--prefix NAME]
91
+ [--duration 1s] [--timing step-end] [--iterations infinite]
92
+ ```
93
+
94
+ `--json` prints the rectangles as JSON, which is the convenient form if you are
95
+ feeding them to an image library such as `sharp` or ImageMagick.
96
+
97
+ ## Why `step-end` and not `steps()`
98
+
99
+ Both work, they just fail differently. `steps(n)` divides one animation cycle
100
+ into n equal slices and needs `background-position` to move linearly across the
101
+ whole sheet, which breaks as soon as frames are trimmed or the last row is
102
+ partial. `step-end` with explicit percentage keyframes pins every frame to a
103
+ known offset, so trimmed frames, ragged last rows and multi-row sheets all work.
104
+ The `data/` folder in this repo shows what three other tools actually ship.
105
+
106
+ ## `data/`
107
+
108
+ - `sprite-sheet-tools-export-formats-2026-09-15.csv` — for each tool that ranked
109
+ for `sprite sheet maker` or `texture packer` on 2026-09-15: what it exports,
110
+ whether a CSS file was actually downloaded, and the literal keyword counts
111
+ inside that file.
112
+ - `serp-top10-2026-09-15.csv` — the raw result lists behind that sample.
113
+
114
+ Read `data/README.md` before quoting any number: it says exactly which cells are
115
+ measured and which are "we looked and did not see one".
116
+
117
+ ## Browser version
118
+
119
+ The same grid and crop math, with a preview and a ZIP download, runs at
120
+ [cutmyimage.com](https://cutmyimage.com) if you would rather not open a
121
+ terminal.
122
+
123
+ ## Tests
124
+
125
+ ```bash
126
+ npm test
127
+ ```
128
+
129
+ 10 tests, no dependencies, `node --test`.
130
+
131
+ ## Licence
132
+
133
+ MIT. The CSV files in `data/` are CC0.
package/bin/cli.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * image-grid-kit CLI
4
+ *
5
+ * node bin/cli.js grid --width 1600 --height 900 --cols 3 --rows 1
6
+ * node bin/cli.js grid --width 1600 --height 900 --mode columns --cols 3 --json
7
+ * node bin/cli.js aspect --width 1600 --height 900 --cols 3 --rows 1 --ratio 4:5
8
+ * node bin/cli.js css --width 192 --height 64 --cols 4 --rows 1 --sheet walk.png --prefix walk
9
+ */
10
+ import { planGrid, planAspect, splitFixed } from "../src/grid.js";
11
+ import { spriteCss } from "../src/css.js";
12
+
13
+ const argv = process.argv.slice(2);
14
+
15
+ function args() {
16
+ const out = {};
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (!a.startsWith("--")) continue;
20
+ const key = a.slice(2);
21
+ const next = argv[i + 1];
22
+ if (next === undefined || next.startsWith("--")) { out[key] = true; }
23
+ else { out[key] = next; i++; }
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function num(v, dflt) {
29
+ const n = Number(v);
30
+ return Number.isFinite(n) ? n : dflt;
31
+ }
32
+
33
+ function help() {
34
+ console.log(`image-grid-kit
35
+
36
+ grid --width N --height N [--mode grid|columns|rows] [--cols N --rows N]
37
+ [--sizing count|pixel --tileW N --tileH N] [--dropPartial] [--json]
38
+ aspect --width N --height N --cols N --rows N [--ratio 4:5] [--json]
39
+ css --width N --height N --cols N --rows N [--sheet FILE] [--prefix NAME]
40
+ [--duration 1s] [--timing step-end] [--iterations infinite]
41
+ `);
42
+ }
43
+
44
+ const cmd = argv[0];
45
+ const a = args();
46
+
47
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") { help(); process.exit(0); }
48
+
49
+ if (cmd === "grid") {
50
+ const res = planGrid({
51
+ width: num(a.width, 0),
52
+ height: num(a.height, 0),
53
+ mode: a.mode || "grid",
54
+ sizing: a.sizing || "count",
55
+ cols: num(a.cols, 3),
56
+ rows: num(a.rows, 3),
57
+ tileW: num(a.tileW, 500),
58
+ tileH: num(a.tileH, 500),
59
+ dropPartial: !!a.dropPartial
60
+ });
61
+ if (a.json) { console.log(JSON.stringify(res, null, 2)); }
62
+ else {
63
+ console.log("xs: " + res.xs.join(", "));
64
+ console.log("ys: " + res.ys.join(", "));
65
+ res.pieces.forEach((p) => {
66
+ console.log(String(p.n).padStart(3) + " x=" + p.x + " y=" + p.y + " w=" + p.w + " h=" + p.h);
67
+ });
68
+ }
69
+ } else if (cmd === "aspect") {
70
+ const ratio = String(a.ratio || "4:5").split(":");
71
+ const res = planAspect({
72
+ width: num(a.width, 0),
73
+ height: num(a.height, 0),
74
+ cols: num(a.cols, 3),
75
+ rows: num(a.rows, 1),
76
+ ratioW: num(ratio[0], 4),
77
+ ratioH: num(ratio[1], 5)
78
+ });
79
+ if (!res) {
80
+ console.log("image too small for a single cell at that ratio");
81
+ process.exit(1);
82
+ }
83
+ if (a.json) { console.log(JSON.stringify(res, null, 2)); }
84
+ else {
85
+ console.log("crop: " + res.crop.w + " x " + res.crop.h + " at (" + res.crop.x + "," + res.crop.y + ")");
86
+ console.log("cut: left " + res.cut.left + " right " + res.cut.right +
87
+ " top " + res.cut.top + " bottom " + res.cut.bottom);
88
+ console.log("each piece: " + res.pieceW + " x " + res.pieceH);
89
+ }
90
+ } else if (cmd === "css") {
91
+ const res = planGrid({
92
+ width: num(a.width, 0),
93
+ height: num(a.height, 0),
94
+ mode: "grid",
95
+ sizing: "count",
96
+ cols: num(a.cols, 4),
97
+ rows: num(a.rows, 1)
98
+ });
99
+ process.stdout.write(spriteCss({
100
+ pieces: res.pieces,
101
+ sheetUrl: a.sheet || "spritesheet.png",
102
+ prefix: a.prefix || "sprite",
103
+ duration: a.duration || "1s",
104
+ timing: a.timing || "step-end",
105
+ iterations: a.iterations || "infinite"
106
+ }));
107
+ } else {
108
+ help();
109
+ process.exit(1);
110
+ }
package/data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Data in this folder
2
+
3
+ Two small CSV files, both produced by hand on 2026-09-15. Read this before
4
+ quoting any number from them.
5
+
6
+ ## How the numbers were produced
7
+
8
+ - Browser: a normal desktop Chrome (Windows 11), driven over the DevTools
9
+ protocol. Not a headless crawler.
10
+ - Search results: `google.com/search?num=20&hl=en&gl=us`, one snapshot per
11
+ keyword, taken 2026-09-15 around 16:20 GMT+8. Rank in the CSV is the order
12
+ the `<h3>` headings appeared in the page source.
13
+ - Tool exports: four 64x64 PNG frames were uploaded to each tool that accepts
14
+ files. Where a CSS export existed, the download was clicked for real and the
15
+ file that landed on disk was opened and counted.
16
+ - Counts (`at_keyframes`, `animation`, `steps_open`, `background_position`) are
17
+ literal substring counts inside the downloaded `.css` text. Nothing was
18
+ parsed or interpreted.
19
+
20
+ ## What is verified and what is not
21
+
22
+ - `css_export = yes` and a `css_bytes` value: a file was really downloaded and
23
+ counted. Verified.
24
+ - `css_export = unknown`: the tool was opened, but no CSS option was seen. This
25
+ is an absence of evidence, not evidence of absence. Do not read it as "this
26
+ tool has no CSS export".
27
+ - `export_formats_observed = not observed`: the page gave nothing to read. Same
28
+ caveat.
29
+ - `steps_open` counts the literal string `steps(`. A tool can animate a sprite
30
+ sheet with `step-end` instead, which this column does not count. The
31
+ cutmyimage.com row shows exactly that: `steps_open` 0 but `animation` 3.
32
+
33
+ ## Known limits
34
+
35
+ - One snapshot, one location setting, one day. Search results move.
36
+ - The 14 tool rows come from the two result pages above, so they are a sample
37
+ of what ranked that day, not a census of every sprite sheet tool.
38
+ - `texture packer` returned 10 headings, but number 2 is a Google Play app
39
+ block, not a web page. Natural web results for that keyword: 9. Three of
40
+ those 9 are sub-pages of codeandweb.com, so distinct hosts: 7.
41
+ - `sprite sheet maker` returned 10 headings, all natural web results, 10
42
+ distinct hosts.
43
+
44
+ ## Licence
45
+
46
+ The CSV files are released under CC0. Quote them freely, ideally with the date.
@@ -0,0 +1,21 @@
1
+ keyword,rank,title,host,url,result_type,note,measured_on
2
+ sprite sheet maker,1,Sprite Sheet Maker,www.finalparsec.com,https://www.finalparsec.com/tools/sprite_sheet_maker,natural,"top block of the results page",2026-09-15
3
+ sprite sheet maker,2,Spritesheet Generator - Create Game Sprite Sheets Online,spritesheetgenerator.online,https://spritesheetgenerator.online/,natural,,2026-09-15
4
+ sprite sheet maker,3,Sprite Sheet Maker & Generator — Create Sprites Instantly,spritesheetmaker.com,https://spritesheetmaker.com/,natural,,2026-09-15
5
+ sprite sheet maker,4,Free Sprite Sheet Packer & Generator,www.codeandweb.com,https://www.codeandweb.com/free-sprite-sheet-packer,natural,,2026-09-15
6
+ sprite sheet maker,5,Piskel - Free online sprite editor,www.piskelapp.com,https://www.piskelapp.com/,natural,,2026-09-15
7
+ sprite sheet maker,6,PixelLab - AI Generator for Pixel Art Game Assets,www.pixellab.ai,https://www.pixellab.ai/,natural,,2026-09-15
8
+ sprite sheet maker,7,Images to Sprite Sheet Generator - Texture Atlas Maker,codeshack.io,https://codeshack.io/images-sprite-sheet-generator/,natural,,2026-09-15
9
+ sprite sheet maker,8,The Spriters Toolkit,tools.spriters-resource.com,https://tools.spriters-resource.com/,natural,,2026-09-15
10
+ sprite sheet maker,9,AI Sprite Sheet Generator - AI Tool,layer.ai,https://layer.ai/tools/layer--generate-a-sprite-sheet,natural,,2026-09-15
11
+ sprite sheet maker,10,Tool to create sprite sheets : r/gamedev,www.reddit.com,https://www.reddit.com/r/gamedev/comments/126lek8/tool_to_create_sprite_sheets/,natural,,2026-09-15
12
+ texture packer,1,TexturePacker - Create Sprite Sheets for your game!,www.codeandweb.com,https://www.codeandweb.com/texturepacker,natural,"top block of the results page",2026-09-15
13
+ texture packer,2,Texture Packer Maker - Apps on Google Play,play.google.com,https://play.google.com/store/apps/details,module,"app pack block, not a web page result - excluded from the natural count",2026-09-15
14
+ texture packer,3,Download TexturePacker Free Trial - Windows Mac Linux,www.codeandweb.com,https://www.codeandweb.com/texturepacker/download,natural,,2026-09-15
15
+ texture packer,4,TexturePacker Online (free) - by CodeAndWeb,www.codeandweb.com,https://www.codeandweb.com/tp-online,natural,,2026-09-15
16
+ texture packer,5,TexturePacker and Unity - Sprite sheets and normal maps,www.codeandweb.com,https://www.codeandweb.com/texturepacker/for-unity,natural,,2026-09-15
17
+ texture packer,6,Texture packer,libgdx.com,https://libgdx.com/wiki/tools/texture-packer,natural,,2026-09-15
18
+ texture packer,7,benbaker76/TexturePacker,github.com,https://github.com/benbaker76/TexturePacker,natural,,2026-09-15
19
+ texture packer,8,I just released a free alternative to Texture Packer on itch.io,www.reddit.com,https://www.reddit.com/r/gamedev/comments/1027zx0/i_just_released_a_free_alternative_to_texture/,natural,,2026-09-15
20
+ texture packer,9,Texture Packer | Sprite Management,assetstore.unity.com,https://assetstore.unity.com/packages/tools/sprite-management/texture-packer-264773,natural,,2026-09-15
21
+ texture packer,10,Online Texture Packer | Create Sprite sheet free for your game,www.toolbuddy.io,https://www.toolbuddy.io/texturepacker,natural,,2026-09-15
@@ -0,0 +1,16 @@
1
+ keyword,name,host,url,kind,export_formats_observed,css_export,css_file_downloaded,css_bytes,at_keyframes,animation,steps_open,background_position,evidence,measured_on
2
+ sprite sheet maker,spritesheetgenerator.online,spritesheetgenerator.online,https://spritesheetgenerator.online/,web tool,"PNG, JSON, ZIP, GIF — quoted from the page: ""Export as PNG, or JSON, ZIP, or GIF if your engine prefers it.""",unknown,no,,,,,,"real Chrome read; 4 test frames uploaded; export area reached; no CSS option seen",2026-09-15
3
+ sprite sheet maker,finalparsec,www.finalparsec.com,https://www.finalparsec.com/tools/sprite_sheet_maker,web tool,"CSS file downloaded (sprite_sheet.css) plus the packed image",yes,yes,644,0,0,0,4,"file downloaded and opened; counts are literal string counts in the CSS text",2026-09-15
4
+ sprite sheet maker,CodeAndWeb free sprite sheet packer,www.codeandweb.com,https://www.codeandweb.com/free-sprite-sheet-packer,web tool,"JSON (hash) seen in the export area; no CSS option seen",unknown,no,,,,,,"real Chrome read; 4 test frames uploaded",2026-09-15
5
+ sprite sheet maker,spritesheetmaker.com,spritesheetmaker.com,https://spritesheetmaker.com/,web tool,"PNG + JSON + CSS + XML (export format toggles on the page)",yes,yes,473,0,0,0,4,"spritesheet.zip downloaded by clicking Download ZIP in a real Chrome; counts from spritesheet.css",2026-09-15
6
+ sprite sheet maker,codeshack,codeshack.io,https://codeshack.io/images-sprite-sheet-generator/,web tool,not observed,unknown,no,,,,,,"page opened; no file input and no export control found, so nothing could be downloaded",2026-09-15
7
+ sprite sheet maker,sprite-ai.art,www.sprite-ai.art,https://www.sprite-ai.art/tools/sprite-sheet-maker,AI site,"PNG, or PNG + JSON together in a ZIP — quoted from the page",unknown,no,,,,,,"real Chrome read; 4 test frames uploaded",2026-09-15
8
+ sprite sheet maker,pixellab.ai,www.pixellab.ai,https://www.pixellab.ai/,AI site,not observed,unknown,no,,,,,,"page opened; export sits behind sign-up, not measured",2026-09-15
9
+ texture packer,CodeAndWeb TexturePacker,www.codeandweb.com,https://www.codeandweb.com/texturepacker,desktop software,"many engine data formats; page says you can also create your own custom format",unknown,no,,,,,,"page text read; desktop product, nothing to download from the page",2026-09-15
10
+ texture packer,CodeAndWeb TexturePacker download page,www.codeandweb.com,https://www.codeandweb.com/texturepacker/download,download page,not applicable,unknown,no,,,,,,"page text read (changelog); no export area on the page",2026-09-15
11
+ texture packer,TexturePacker Online,www.codeandweb.com,https://www.codeandweb.com/tp-online,web tool,"JSON (a Format control is present); no CSS option seen",unknown,no,,,,,,"real Chrome read; 4 test frames uploaded",2026-09-15
12
+ texture packer,free-tex-packer (odrick),github.com,https://github.com/odrick/free-tex-packer,open-source repo,not measured,unknown,no,,,,,,"repository page read; package not installed or run",2026-09-15
13
+ texture packer,libGDX texture packer wiki,libgdx.com,https://libgdx.com/wiki/tools/texture-packer,documentation,not applicable,unknown,no,,,,,,"documentation page read",2026-09-15
14
+ texture packer,ToolBuddy Texture Packer,www.toolbuddy.io,https://www.toolbuddy.io/texturepacker,web tool,"16 export presets incl. CSS (exporter dropdown)",yes,yes,659,0,0,0,4,"atlas.zip downloaded by clicking Export package then Export ZIP in a real Chrome; counts from atlas.sprites.css",2026-09-15
15
+ texture packer,Texture Packer (Unity Asset Store),assetstore.unity.com,https://assetstore.unity.com/packages/tools/sprite-management/texture-packer-264773,store plugin,not measured,unknown,no,,,,,,"store page read; plugin not installed",2026-09-15
16
+ n/a,cutmyimage.com sprite sheet maker,cutmyimage.com,https://cutmyimage.com/sprite-sheet-maker/,web tool (this project's site),"PNG, JSON, CSS",yes,yes,913,1,3,0,9,"our own export, downloaded and counted the same way; the animation uses step-end, not steps()",2026-09-15
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "image-grid-kit",
3
+ "version": "1.0.0",
4
+ "description": "Zero-dependency helpers for splitting an image into a grid and generating CSS sprite-sheet animations from the resulting coordinates.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./grid": "./src/grid.js",
10
+ "./css": "./src/css.js"
11
+ },
12
+ "bin": {
13
+ "image-grid-kit": "bin/cli.js"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "bin",
18
+ "data"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test test/",
22
+ "cli": "node bin/cli.js"
23
+ },
24
+ "keywords": [
25
+ "image",
26
+ "split",
27
+ "grid",
28
+ "sprite",
29
+ "spritesheet",
30
+ "css",
31
+ "keyframes",
32
+ "texture-atlas"
33
+ ],
34
+ "homepage": "https://cutmyimage.com",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/jessica7168295-ux/image-grid-kit.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/jessica7168295-ux/image-grid-kit/issues"
41
+ },
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }
package/src/css.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * image-grid-kit — CSS sprite-sheet output.
3
+ *
4
+ * Takes the piece rectangles from planGrid() (or any list of
5
+ * {name, x, y, w, h}) and writes:
6
+ * - one class per frame, positioned with background-position
7
+ * - one @keyframes block that steps through the frames
8
+ * - one animation class
9
+ *
10
+ * The `step-end` timing function is what makes the sheet play frame by frame
11
+ * instead of sliding between frames.
12
+ */
13
+
14
+ function slug(s) {
15
+ return String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "frame";
16
+ }
17
+
18
+ function pad2(n) { return n < 10 ? "0" + n : String(n); }
19
+
20
+ function pct(i, n) {
21
+ const v = (i / n) * 100;
22
+ return String(Number(v.toFixed(3)));
23
+ }
24
+
25
+ /**
26
+ * @param {object} o
27
+ * @param {Array<{name?:string,x:number,y:number,w:number,h:number}>} o.pieces
28
+ * @param {string} [o.sheetUrl] url() value, defaults to "spritesheet.png"
29
+ * @param {string} [o.prefix] class prefix, defaults to "sprite"
30
+ * @param {string} [o.duration] animation-duration, defaults to "1s"
31
+ * @param {string} [o.timing] animation-timing-function, defaults to "step-end"
32
+ * @param {string} [o.iterations] animation-iteration-count, defaults to "infinite"
33
+ * @returns {string} CSS text
34
+ */
35
+ export function spriteCss(o) {
36
+ const pieces = (o.pieces || []).slice();
37
+ if (!pieces.length) throw new Error("spriteCss: no pieces given");
38
+ const sheetUrl = o.sheetUrl || "spritesheet.png";
39
+ const prefix = slug(o.prefix || "sprite");
40
+ const duration = o.duration || "1s";
41
+ const timing = o.timing || "step-end";
42
+ const iterations = o.iterations || "infinite";
43
+ const n = pieces.length;
44
+
45
+ const lines = [];
46
+ lines.push("/* Generated by image-grid-kit */");
47
+ lines.push("." + prefix + " {");
48
+ lines.push(" background-image: url('" + sheetUrl + "');");
49
+ lines.push(" background-repeat: no-repeat;");
50
+ lines.push(" display: inline-block;");
51
+ lines.push("}");
52
+ lines.push("");
53
+
54
+ pieces.forEach((p, i) => {
55
+ const nm = p.name ? slug(p.name) : "frame-" + pad2(i + 1);
56
+ lines.push("." + prefix + "." + prefix + "-" + nm + " {");
57
+ lines.push(" width: " + p.w + "px;");
58
+ lines.push(" height: " + p.h + "px;");
59
+ lines.push(" background-position: -" + p.x + "px -" + p.y + "px;");
60
+ lines.push("}");
61
+ });
62
+
63
+ lines.push("");
64
+ lines.push("/* Frame-by-frame playback: change animation-duration to taste. */");
65
+ lines.push("@keyframes " + prefix + "-play {");
66
+ pieces.forEach((p, i) => {
67
+ lines.push(" " + pct(i, n) + "% { background-position: -" + p.x + "px -" + p.y + "px; }");
68
+ });
69
+ const last = pieces[n - 1];
70
+ lines.push(" 100% { background-position: -" + last.x + "px -" + last.y + "px; }");
71
+ lines.push("}");
72
+ lines.push("." + prefix + "-anim {");
73
+ lines.push(" width: " + pieces[0].w + "px;");
74
+ lines.push(" height: " + pieces[0].h + "px;");
75
+ lines.push(" animation: " + prefix + "-play " + duration + " " + timing + " " + iterations + ";");
76
+ lines.push("}");
77
+ return lines.join("\n") + "\n";
78
+ }
79
+
80
+ export { slug };
package/src/grid.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * image-grid-kit — grid math.
3
+ *
4
+ * These two functions mirror the behaviour of the browser tool at
5
+ * https://cutmyimage.com so that a script and the web page agree on where
6
+ * the cut lines fall.
7
+ */
8
+
9
+ /**
10
+ * Split `total` pixels into `n` integer parts. The first `total % n` parts
11
+ * get one extra pixel, so the parts always sum back to `total`.
12
+ *
13
+ * splitEven(900, 3) -> [300, 300, 300]
14
+ * splitEven(1600, 3) -> [534, 533, 533]
15
+ */
16
+ export function splitEven(total, n) {
17
+ const t = Math.floor(total);
18
+ const count = Math.floor(n);
19
+ if (!(t > 0) || !(count > 0)) return [];
20
+ const base = Math.floor(t / count);
21
+ const rem = t % count;
22
+ const out = [];
23
+ for (let i = 0; i < count; i++) out.push(base + (i < rem ? 1 : 0));
24
+ return out;
25
+ }
26
+
27
+ /**
28
+ * Split `total` pixels into parts of at most `step` pixels. The last part is
29
+ * the remainder and may be shorter.
30
+ *
31
+ * splitFixed(1000, 300) -> [300, 300, 300, 100]
32
+ */
33
+ export function splitFixed(total, step) {
34
+ const t = Math.floor(total);
35
+ const s = Math.floor(step);
36
+ if (!(t > 0) || !(s > 0)) return [];
37
+ const out = [];
38
+ let left = t;
39
+ while (left > 0) {
40
+ const part = Math.min(s, left);
41
+ out.push(part);
42
+ left -= part;
43
+ }
44
+ return out.length ? out : [t];
45
+ }
46
+
47
+ /**
48
+ * Lay a grid over an image and return one rectangle per piece.
49
+ *
50
+ * @param {object} o
51
+ * @param {number} o.width image width in px
52
+ * @param {number} o.height image height in px
53
+ * @param {'grid'|'columns'|'rows'} [o.mode]
54
+ * @param {'count'|'pixel'} [o.sizing] 'count' = number of pieces, 'pixel' = fixed cell size
55
+ * @param {number} [o.cols]
56
+ * @param {number} [o.rows]
57
+ * @param {number} [o.tileW] used when sizing === 'pixel'
58
+ * @param {number} [o.tileH] used when sizing === 'pixel'
59
+ * @param {boolean} [o.dropPartial] with sizing 'pixel': drop the short remainder piece
60
+ * @returns {{xs:number[], ys:number[], pieces:Array<{n:number,x:number,y:number,w:number,h:number}>}}
61
+ */
62
+ export function planGrid(o) {
63
+ const width = Math.floor(o.width);
64
+ const height = Math.floor(o.height);
65
+ const mode = o.mode || "grid";
66
+ const sizing = o.sizing || "count";
67
+ let xs, ys;
68
+
69
+ if (sizing === "count") {
70
+ const cols = Math.max(1, Math.floor(o.cols || 1));
71
+ const rows = Math.max(1, Math.floor(o.rows || 1));
72
+ if (mode === "columns") { xs = splitEven(width, cols); ys = [height]; }
73
+ else if (mode === "rows") { xs = [width]; ys = splitEven(height, rows); }
74
+ else { xs = splitEven(width, cols); ys = splitEven(height, rows); }
75
+ } else {
76
+ const tileW = Math.max(1, Math.floor(o.tileW || 1));
77
+ const tileH = Math.max(1, Math.floor(o.tileH || 1));
78
+ if (mode === "columns") { xs = splitFixed(width, tileW); ys = [height]; }
79
+ else if (mode === "rows") { xs = [width]; ys = splitFixed(height, tileH); }
80
+ else { xs = splitFixed(width, tileW); ys = splitFixed(height, tileH); }
81
+ if (o.dropPartial) {
82
+ if (mode !== "rows") xs = xs.filter((v, i) => !(i === xs.length - 1 && v < tileW));
83
+ if (mode !== "columns") ys = ys.filter((v, i) => !(i === ys.length - 1 && v < tileH));
84
+ }
85
+ }
86
+
87
+ const pieces = [];
88
+ let n = 0;
89
+ let y = 0;
90
+ for (let r = 0; r < ys.length; r++) {
91
+ let x = 0;
92
+ for (let c = 0; c < xs.length; c++) {
93
+ n += 1;
94
+ pieces.push({ n, x, y, w: xs[c], h: ys[r] });
95
+ x += xs[c];
96
+ }
97
+ y += ys[r];
98
+ }
99
+ return { xs, ys, pieces };
100
+ }
101
+
102
+ /**
103
+ * Plan the largest centre crop of `width` x `height` whose aspect ratio is
104
+ * exactly (ratioW x cols) : (ratioH x rows), so that every piece of the
105
+ * `cols` x `rows` grid comes out at exactly ratioW : ratioH.
106
+ *
107
+ * This is how the 4:5 (1080 x 1350) mode works on the Instagram page:
108
+ * planAspect({width:1600, height:900, rows:1, cols:3, ratioW:4, ratioH:5})
109
+ * -> crop 1596 x 665, 4 px cut left and right, 235 px cut top and bottom,
110
+ * each piece 532 x 665.
111
+ *
112
+ * @returns {null|object} null when the image is too small for even one cell
113
+ */
114
+ export function planAspect(o) {
115
+ const w = Math.floor(o.width);
116
+ const h = Math.floor(o.height);
117
+ const rows = Math.max(1, Math.floor(o.rows || 1));
118
+ const cols = Math.max(1, Math.floor(o.cols || 1));
119
+ const rw = Math.floor(o.ratioW || 4);
120
+ const rh = Math.floor(o.ratioH || 5);
121
+ const aw = rw * cols;
122
+ const ah = rh * rows;
123
+ const k = Math.floor(Math.min(w / aw, h / ah));
124
+ if (!(k >= 1)) return null;
125
+ const cw = aw * k;
126
+ const ch = ah * k;
127
+ const sx = Math.floor((w - cw) / 2);
128
+ const sy = Math.floor((h - ch) / 2);
129
+ return {
130
+ crop: { x: sx, y: sy, w: cw, h: ch },
131
+ cut: { left: sx, right: w - cw - sx, top: sy, bottom: h - ch - sy },
132
+ pieceW: rw * k,
133
+ pieceH: rh * k
134
+ };
135
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { splitEven, splitFixed, planGrid, planAspect } from "./grid.js";
2
+ export { spriteCss, slug } from "./css.js";