fablecut 1.13.0 → 1.14.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/CLAUDE.md +429 -5
- package/README.md +23 -2
- package/app.js +1965 -103
- package/comp.js +195 -0
- package/edit-ops.js +50 -18
- package/index.html +146 -68
- package/library/README.md +2 -0
- package/library/elements/logo-placeholder.svg +12 -0
- package/mcp-server.js +437 -17
- package/motion.js +803 -0
- package/package.json +1 -1
- package/paths.js +25 -2
- package/server.js +20 -1
- package/style.css +177 -24
- package/templates.js +576 -0
package/package.json
CHANGED
package/paths.js
CHANGED
|
@@ -33,7 +33,7 @@ const EXPORTS_DIR = path.join(DATA_DIR, "exports");
|
|
|
33
33
|
const ANALYSIS_DIR = path.join(DATA_DIR, "analysis");
|
|
34
34
|
const LIBRARY_DIR = path.join(DATA_DIR, "library");
|
|
35
35
|
const PROJECT_FILE = path.join(DATA_DIR, "project.json");
|
|
36
|
-
const LIBRARY_SUBDIRS = ["sfx", "elements", "svg", "fonts"];
|
|
36
|
+
const LIBRARY_SUBDIRS = ["sfx", "elements", "svg", "fonts", "presets", "templates"];
|
|
37
37
|
|
|
38
38
|
/* The asset library ships with the repo but users also drop their own files in
|
|
39
39
|
(library/sfx is gitignored precisely for that). When the data dir is split
|
|
@@ -99,8 +99,31 @@ function ensureDirs() {
|
|
|
99
99
|
seedLibrary();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/* Saved animation presets and templates: one JSON file each in library/presets/
|
|
103
|
+
and library/templates/ (templates.js validates them). Unreadable files are skipped. */
|
|
104
|
+
function readSaved(kind) {
|
|
105
|
+
const dir = path.join(LIBRARY_DIR, kind);
|
|
106
|
+
let names = [];
|
|
107
|
+
try { names = fs.readdirSync(dir).filter((f) => f.toLowerCase().endsWith(".json")).sort(); } catch { return []; }
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const f of names) {
|
|
110
|
+
try { out.push({ file: f, data: JSON.parse(fs.readFileSync(path.join(dir, f), "utf8").replace(new RegExp("^\\uFEFF"), "")) }); } catch { }
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function writeSaved(kind, id, data) {
|
|
115
|
+
const dir = path.join(LIBRARY_DIR, kind);
|
|
116
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
117
|
+
const file = path.join(dir, String(id).replace(/[^a-z0-9-]+/gi, "-").slice(0, 60) + ".json");
|
|
118
|
+
// tmp + rename, like the project file: a crash mid-write never leaves half a preset
|
|
119
|
+
const tmp = `${file}.${crypto.randomUUID()}.tmp`; // unique: the editor and the MCP server may save at once
|
|
120
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
121
|
+
fs.renameSync(tmp, file);
|
|
122
|
+
return file;
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
module.exports = {
|
|
103
126
|
APP_DIR, DATA_DIR, SPLIT,
|
|
104
127
|
MEDIA_DIR, EXPORTS_DIR, ANALYSIS_DIR, LIBRARY_DIR, PROJECT_FILE,
|
|
105
|
-
LIBRARY_SUBDIRS, ensureDirs,
|
|
128
|
+
LIBRARY_SUBDIRS, ensureDirs, readSaved, writeSaved,
|
|
106
129
|
};
|
package/server.js
CHANGED
|
@@ -37,8 +37,9 @@ const { denoiseFile, AMOUNT_IDS: DENOISE_AMOUNTS } = require("./denoise");
|
|
|
37
37
|
|
|
38
38
|
const {
|
|
39
39
|
APP_DIR, DATA_DIR, MEDIA_DIR, EXPORTS_DIR, ANALYSIS_DIR, LIBRARY_DIR,
|
|
40
|
-
PROJECT_FILE, LIBRARY_SUBDIRS, ensureDirs,
|
|
40
|
+
PROJECT_FILE, LIBRARY_SUBDIRS, ensureDirs, readSaved, writeSaved,
|
|
41
41
|
} = require("./paths");
|
|
42
|
+
const Templates = require("./templates");
|
|
42
43
|
|
|
43
44
|
/* Static app files are served from the install dir; everything the user creates
|
|
44
45
|
lives under DATA_DIR. The two are the same unless FABLECUT_DATA_DIR is set. */
|
|
@@ -466,6 +467,24 @@ const server = http.createServer(async (req, res) => {
|
|
|
466
467
|
return;
|
|
467
468
|
}
|
|
468
469
|
|
|
470
|
+
/* API: saved animation presets and templates (library/presets, library/templates).
|
|
471
|
+
GET /api/presets | /api/templates → [ …validated items… ] (bad files skipped)
|
|
472
|
+
POST /api/presets {preset} | /api/templates {template} → saved as library/<kind>/<id>.json */
|
|
473
|
+
if ((p === "/api/presets" || p === "/api/templates") && (req.method === "GET" || req.method === "POST")) {
|
|
474
|
+
const kind = p.slice(5), norm = kind === "presets" ? Templates.normalizePreset : Templates.normalizeTemplate;
|
|
475
|
+
try {
|
|
476
|
+
if (req.method === "GET") {
|
|
477
|
+
sendJSON(res, 200, readSaved(kind).map((x) => norm(x.data)).filter(Boolean));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const body = JSON.parse((await readBody(req)).toString("utf8") || "{}");
|
|
481
|
+
const item = Templates.asSaved(norm(kind === "presets" ? body.preset : body.template, true), kind);
|
|
482
|
+
const file = writeSaved(kind, item.id, item);
|
|
483
|
+
sendJSON(res, 200, { id: item.id, name: item.name, file: path.relative(DATA_DIR, file).split(path.sep).join("/") });
|
|
484
|
+
} catch (e) { sendJSON(res, 400, { error: String(e.message || e) }); }
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
469
488
|
/* API: upload → saved into ./media */
|
|
470
489
|
if (p === "/api/upload" && req.method === "POST") {
|
|
471
490
|
try {
|
package/style.css
CHANGED
|
@@ -2713,6 +2713,98 @@ body.track-size-s .clip .fade-grip {
|
|
|
2713
2713
|
background: linear-gradient(180deg, #ffd16633, #ffd16611), #6b5420;
|
|
2714
2714
|
}
|
|
2715
2715
|
|
|
2716
|
+
.lib-item .bin-thumb.shape-thumb {
|
|
2717
|
+
display: grid;
|
|
2718
|
+
place-items: center;
|
|
2719
|
+
background: #1d4f63;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
.lib-item .bin-thumb.shape-thumb svg {
|
|
2723
|
+
width: 70%;
|
|
2724
|
+
height: 70%;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
.lib-group {
|
|
2728
|
+
margin: 10px 4px 4px;
|
|
2729
|
+
font-size: 10.5px;
|
|
2730
|
+
font-weight: 600;
|
|
2731
|
+
letter-spacing: 0.06em;
|
|
2732
|
+
text-transform: uppercase;
|
|
2733
|
+
color: var(--dim);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
.lib-group:first-child {
|
|
2737
|
+
margin-top: 2px;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
.lib-hint {
|
|
2741
|
+
margin: 10px 4px;
|
|
2742
|
+
font-size: 11px;
|
|
2743
|
+
line-height: 1.4;
|
|
2744
|
+
color: var(--dim);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
.clip.c-comp .comp-open-hint {
|
|
2748
|
+
position: absolute;
|
|
2749
|
+
right: 6px;
|
|
2750
|
+
top: 50%;
|
|
2751
|
+
transform: translateY(-50%);
|
|
2752
|
+
padding: 2px 7px;
|
|
2753
|
+
border-radius: 999px;
|
|
2754
|
+
background: #00000073;
|
|
2755
|
+
color: #f3e8ff;
|
|
2756
|
+
font-size: 10.5px;
|
|
2757
|
+
white-space: nowrap;
|
|
2758
|
+
pointer-events: none;
|
|
2759
|
+
opacity: 0;
|
|
2760
|
+
transition: opacity 0.15s;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
.clip.c-comp:hover .comp-open-hint {
|
|
2764
|
+
opacity: 1;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
body.track-size-s .clip.c-comp .comp-open-hint {
|
|
2768
|
+
display: none;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
.clip.c-comp .clip-body {
|
|
2772
|
+
background: linear-gradient(180deg, #c084fc33, #c084fc11), #4c2a73;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
.comp-crumb {
|
|
2776
|
+
display: inline-flex;
|
|
2777
|
+
align-items: center;
|
|
2778
|
+
gap: 6px;
|
|
2779
|
+
padding: 2px 8px 2px 2px;
|
|
2780
|
+
margin-right: 6px;
|
|
2781
|
+
border: 1px solid #c084fc66;
|
|
2782
|
+
border-radius: 6px;
|
|
2783
|
+
background: #c084fc1a;
|
|
2784
|
+
font-size: 12px;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
.comp-crumb .crumb-sep {
|
|
2788
|
+
opacity: .6;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
.lib-item .bin-thumb.comp-thumb,
|
|
2792
|
+
.comp-item .bin-thumb.comp-thumb {
|
|
2793
|
+
display: grid;
|
|
2794
|
+
place-items: center;
|
|
2795
|
+
background: #4c2a73;
|
|
2796
|
+
color: #e9d5ff;
|
|
2797
|
+
font-size: 20px;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
.clip.c-shape .clip-body {
|
|
2801
|
+
background: linear-gradient(180deg, #5ad1ff33, #5ad1ff11), #1d4f63;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
.clip.c-null .clip-body {
|
|
2805
|
+
background: repeating-linear-gradient(90deg, #ff6b6b22 0 6px, transparent 6px 12px), #4a2430;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2716
2808
|
.clip.c-adjust .clip-body {
|
|
2717
2809
|
background: repeating-linear-gradient(135deg, #38bdf822 0 10px, transparent 10px 20px), #155e75;
|
|
2718
2810
|
}
|
|
@@ -2949,9 +3041,43 @@ body.track-size-s .clip .fade-grip {
|
|
|
2949
3041
|
}
|
|
2950
3042
|
|
|
2951
3043
|
.dialog.help {
|
|
2952
|
-
width: min(
|
|
2953
|
-
max-height:
|
|
2954
|
-
|
|
3044
|
+
width: min(1080px, 94vw);
|
|
3045
|
+
max-height: 88vh;
|
|
3046
|
+
display: flex;
|
|
3047
|
+
flex-direction: column;
|
|
3048
|
+
padding-bottom: 14px;
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
.dialog.help .help-head {
|
|
3052
|
+
display: flex;
|
|
3053
|
+
align-items: center;
|
|
3054
|
+
gap: 16px;
|
|
3055
|
+
margin-bottom: 12px;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
.dialog.help .help-head h2 {
|
|
3059
|
+
margin: 0;
|
|
3060
|
+
flex: 1;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
.dialog.help .help-filter {
|
|
3064
|
+
width: min(280px, 50%);
|
|
3065
|
+
background: var(--panel-2);
|
|
3066
|
+
border: 1px solid var(--border);
|
|
3067
|
+
border-radius: 6px;
|
|
3068
|
+
color: var(--text);
|
|
3069
|
+
font: inherit;
|
|
3070
|
+
font-size: 12px;
|
|
3071
|
+
padding: 6px 10px;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
.dialog.help .help-filter:focus {
|
|
3075
|
+
outline: none;
|
|
3076
|
+
border-color: var(--accent);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
.dialog.help .dialog-actions {
|
|
3080
|
+
margin-top: 10px;
|
|
2955
3081
|
}
|
|
2956
3082
|
|
|
2957
3083
|
.dialog.settings {
|
|
@@ -3022,11 +3148,38 @@ body.track-size-s .clip .fade-grip {
|
|
|
3022
3148
|
}
|
|
3023
3149
|
|
|
3024
3150
|
.dialog.help .keys-cols {
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3151
|
+
flex: 1;
|
|
3152
|
+
min-height: 0;
|
|
3153
|
+
overflow: auto;
|
|
3154
|
+
columns: 2 380px;
|
|
3155
|
+
column-gap: 16px;
|
|
3156
|
+
padding-right: 4px;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
.dialog.help .keys-sec {
|
|
3160
|
+
break-inside: avoid;
|
|
3161
|
+
margin: 0 0 14px;
|
|
3162
|
+
padding: 10px 12px 6px;
|
|
3163
|
+
background: var(--panel-2);
|
|
3164
|
+
border: 1px solid var(--border);
|
|
3165
|
+
border-radius: 8px;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
.dialog.help .keys-sec h3 {
|
|
3169
|
+
margin: 0 0 6px;
|
|
3170
|
+
font-size: 11px;
|
|
3171
|
+
font-weight: 600;
|
|
3172
|
+
letter-spacing: 0.06em;
|
|
3173
|
+
text-transform: uppercase;
|
|
3174
|
+
color: var(--text);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
.dialog.help .keys-sec h3 span {
|
|
3178
|
+
margin-left: 6px;
|
|
3179
|
+
font-weight: 400;
|
|
3180
|
+
letter-spacing: 0;
|
|
3181
|
+
text-transform: none;
|
|
3182
|
+
color: var(--dim);
|
|
3030
3183
|
}
|
|
3031
3184
|
|
|
3032
3185
|
.dialog.help .keys {
|
|
@@ -3036,22 +3189,31 @@ body.track-size-s .clip .fade-grip {
|
|
|
3036
3189
|
}
|
|
3037
3190
|
|
|
3038
3191
|
.dialog.help .keys td {
|
|
3039
|
-
padding: 5px
|
|
3040
|
-
border-
|
|
3192
|
+
padding: 5px 0;
|
|
3193
|
+
border-top: 1px solid var(--border);
|
|
3041
3194
|
font-size: 12px;
|
|
3195
|
+
line-height: 1.45;
|
|
3042
3196
|
vertical-align: top;
|
|
3043
3197
|
}
|
|
3044
3198
|
|
|
3199
|
+
.dialog.help .keys tr:first-child td {
|
|
3200
|
+
border-top: 0;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3045
3203
|
.dialog.help .keys td:first-child {
|
|
3046
|
-
width:
|
|
3204
|
+
width: 34%;
|
|
3205
|
+
padding-right: 12px;
|
|
3047
3206
|
color: var(--dim);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
.dialog.help .keys td:first-child kbd {
|
|
3048
3210
|
white-space: nowrap;
|
|
3049
3211
|
}
|
|
3050
3212
|
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3213
|
+
.dialog.help .help-empty {
|
|
3214
|
+
color: var(--dim);
|
|
3215
|
+
font-size: 12px;
|
|
3216
|
+
margin: 8px 2px;
|
|
3055
3217
|
}
|
|
3056
3218
|
|
|
3057
3219
|
kbd {
|
|
@@ -3563,12 +3725,3 @@ input[type="range"].cq-hue {
|
|
|
3563
3725
|
border-radius: 3px;
|
|
3564
3726
|
height: 6px;
|
|
3565
3727
|
}
|
|
3566
|
-
.dialog.help .keys tr.keys-head td {
|
|
3567
|
-
padding-top: 12px;
|
|
3568
|
-
color: var(--text);
|
|
3569
|
-
font-weight: 600;
|
|
3570
|
-
font-size: 11px;
|
|
3571
|
-
letter-spacing: 0.04em;
|
|
3572
|
-
text-transform: uppercase;
|
|
3573
|
-
white-space: normal;
|
|
3574
|
-
}
|