@autumnsgrove/groveengine 0.8.5 → 0.9.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/dist/components/WispPanel.svelte +0 -1
- package/dist/components/admin/GutterManager.svelte +213 -101
- package/dist/components/admin/MarkdownEditor.svelte +6 -3
- package/dist/components/custom/ContentWithGutter.svelte +7 -13
- package/dist/components/custom/GutterItem.svelte +8 -2
- package/dist/components/quota/UpgradePrompt.svelte +1 -0
- package/dist/config/domain-blocklist.d.ts +59 -0
- package/dist/config/domain-blocklist.js +731 -0
- package/dist/config/index.d.ts +3 -1
- package/dist/config/index.js +2 -1
- package/dist/config/offensive-blocklist.d.ts +44 -0
- package/dist/config/offensive-blocklist.js +751 -0
- package/dist/config/terrarium.d.ts +109 -0
- package/dist/config/terrarium.js +125 -0
- package/dist/styles/tokens.css +90 -0
- package/dist/types/dom-to-image-more.d.ts +39 -0
- package/dist/ui/components/chrome/Footer.svelte +137 -0
- package/dist/ui/components/chrome/Footer.svelte.d.ts +11 -0
- package/dist/ui/components/chrome/FooterMinimal.svelte +75 -0
- package/dist/ui/components/chrome/FooterMinimal.svelte.d.ts +10 -0
- package/dist/ui/components/chrome/Header.svelte +113 -0
- package/dist/ui/components/chrome/Header.svelte.d.ts +11 -0
- package/dist/ui/components/chrome/HeaderMinimal.svelte +68 -0
- package/dist/ui/components/chrome/HeaderMinimal.svelte.d.ts +9 -0
- package/dist/ui/components/chrome/MobileMenu.svelte +145 -0
- package/dist/ui/components/chrome/MobileMenu.svelte.d.ts +9 -0
- package/dist/ui/components/chrome/ThemeToggle.svelte +34 -0
- package/dist/ui/components/chrome/ThemeToggle.svelte.d.ts +3 -0
- package/dist/ui/components/chrome/defaults.d.ts +6 -0
- package/dist/ui/components/chrome/defaults.js +65 -0
- package/dist/ui/components/chrome/index.d.ts +13 -0
- package/dist/ui/components/chrome/index.js +14 -0
- package/dist/ui/components/chrome/types.d.ts +19 -0
- package/dist/ui/components/chrome/types.js +8 -0
- package/dist/ui/components/content/RoadmapPreview.svelte +2 -1
- package/dist/ui/components/forms/ContentSearch.svelte +406 -0
- package/dist/ui/components/forms/ContentSearch.svelte.d.ts +71 -0
- package/dist/ui/components/forms/SearchInput.svelte +0 -1
- package/dist/ui/components/forms/filterUtils.d.ts +138 -0
- package/dist/ui/components/forms/filterUtils.js +240 -0
- package/dist/ui/components/forms/index.d.ts +2 -0
- package/dist/ui/components/forms/index.js +5 -1
- package/dist/ui/components/gallery/ImageGallery.svelte +17 -3
- package/dist/ui/components/gallery/Lightbox.svelte +11 -3
- package/dist/ui/components/gallery/ZoomableImage.svelte +13 -2
- package/dist/ui/components/icons/index.d.ts +2 -1
- package/dist/ui/components/icons/index.js +14 -3
- package/dist/ui/components/icons/lucide.d.ts +213 -0
- package/dist/ui/components/icons/lucide.js +224 -0
- package/dist/ui/components/terrarium/AssetPalette.svelte +207 -0
- package/dist/ui/components/terrarium/AssetPalette.svelte.d.ts +7 -0
- package/dist/ui/components/terrarium/Canvas.svelte +231 -0
- package/dist/ui/components/terrarium/Canvas.svelte.d.ts +14 -0
- package/dist/ui/components/terrarium/ExportDialog.svelte +307 -0
- package/dist/ui/components/terrarium/ExportDialog.svelte.d.ts +18 -0
- package/dist/ui/components/terrarium/PaletteItem.svelte +169 -0
- package/dist/ui/components/terrarium/PaletteItem.svelte.d.ts +9 -0
- package/dist/ui/components/terrarium/PlacedAsset.svelte +222 -0
- package/dist/ui/components/terrarium/PlacedAsset.svelte.d.ts +11 -0
- package/dist/ui/components/terrarium/Terrarium.svelte +266 -0
- package/dist/ui/components/terrarium/Terrarium.svelte.d.ts +3 -0
- package/dist/ui/components/terrarium/Toolbar.svelte +299 -0
- package/dist/ui/components/terrarium/Toolbar.svelte.d.ts +24 -0
- package/dist/ui/components/terrarium/index.d.ts +31 -0
- package/dist/ui/components/terrarium/index.js +33 -0
- package/dist/ui/components/terrarium/terrariumState.svelte.d.ts +45 -0
- package/dist/ui/components/terrarium/terrariumState.svelte.js +291 -0
- package/dist/ui/components/terrarium/types.d.ts +139 -0
- package/dist/ui/components/terrarium/types.js +43 -0
- package/dist/ui/components/terrarium/utils/export.d.ts +48 -0
- package/dist/ui/components/terrarium/utils/export.js +148 -0
- package/dist/ui/components/typography/index.d.ts +0 -10
- package/dist/ui/components/typography/index.js +1 -12
- package/dist/ui/components/ui/CollapsibleSection.svelte +12 -0
- package/dist/ui/components/ui/GlassConfirmDialog.svelte +9 -0
- package/dist/ui/components/ui/GlassOverlay.svelte +2 -1
- package/dist/ui/components/ui/Input.svelte +9 -1
- package/dist/ui/components/ui/Input.svelte.d.ts +2 -0
- package/dist/ui/components/ui/Textarea.svelte +9 -1
- package/dist/ui/components/ui/Textarea.svelte.d.ts +2 -0
- package/dist/ui/stores/index.d.ts +6 -0
- package/dist/ui/stores/index.js +6 -0
- package/dist/ui/stores/season.d.ts +14 -0
- package/dist/ui/stores/season.js +65 -0
- package/dist/ui/tokens/fonts.d.ts +1 -1
- package/dist/ui/tokens/fonts.js +0 -126
- package/package.json +46 -22
- package/static/fonts/alagard.ttf +0 -0
- package/LICENSE +0 -378
- package/dist/ui/components/typography/BodoniModa.svelte +0 -17
- package/dist/ui/components/typography/BodoniModa.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Cormorant.svelte +0 -17
- package/dist/ui/components/typography/Cormorant.svelte.d.ts +0 -10
- package/dist/ui/components/typography/EBGaramond.svelte +0 -17
- package/dist/ui/components/typography/EBGaramond.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Fraunces.svelte +0 -17
- package/dist/ui/components/typography/Fraunces.svelte.d.ts +0 -10
- package/dist/ui/components/typography/InstrumentSans.svelte +0 -17
- package/dist/ui/components/typography/InstrumentSans.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Lora.svelte +0 -17
- package/dist/ui/components/typography/Lora.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Luciole.svelte +0 -17
- package/dist/ui/components/typography/Luciole.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Manrope.svelte +0 -17
- package/dist/ui/components/typography/Manrope.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Merriweather.svelte +0 -17
- package/dist/ui/components/typography/Merriweather.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Nunito.svelte +0 -17
- package/dist/ui/components/typography/Nunito.svelte.d.ts +0 -10
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grove — A place to Be
|
|
3
|
+
* Copyright (c) 2025 Autumn Brown
|
|
4
|
+
* Licensed under AGPL-3.0
|
|
5
|
+
*/
|
|
6
|
+
import { DEFAULT_SCENE } from './types';
|
|
7
|
+
import { TERRARIUM_CONFIG } from '../../../config/terrarium';
|
|
8
|
+
/**
|
|
9
|
+
* Calculate complexity cost of a placed asset
|
|
10
|
+
*/
|
|
11
|
+
function calculateAssetComplexity(asset) {
|
|
12
|
+
let cost = TERRARIUM_CONFIG.complexity.weights.normal;
|
|
13
|
+
if (asset.animationEnabled) {
|
|
14
|
+
cost = TERRARIUM_CONFIG.complexity.weights.animated;
|
|
15
|
+
}
|
|
16
|
+
else if (asset.scale > 1.5 || asset.scale < 0.5) {
|
|
17
|
+
cost = TERRARIUM_CONFIG.complexity.weights.scaled;
|
|
18
|
+
}
|
|
19
|
+
return cost;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Calculate total scene complexity
|
|
23
|
+
*/
|
|
24
|
+
function calculateSceneComplexity(assets) {
|
|
25
|
+
return assets.reduce((total, asset) => total + calculateAssetComplexity(asset), 0);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a new empty scene
|
|
29
|
+
*/
|
|
30
|
+
function createEmptyScene() {
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
return {
|
|
33
|
+
...DEFAULT_SCENE,
|
|
34
|
+
id: crypto.randomUUID(),
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the highest z-index in the scene
|
|
41
|
+
*/
|
|
42
|
+
function getMaxZIndex(assets) {
|
|
43
|
+
if (assets.length === 0)
|
|
44
|
+
return 0;
|
|
45
|
+
return Math.max(...assets.map((a) => a.zIndex));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create the Terrarium state manager
|
|
49
|
+
*/
|
|
50
|
+
export function createTerrariumState() {
|
|
51
|
+
let scene = $state(createEmptyScene());
|
|
52
|
+
let selectedAssetId = $state(null);
|
|
53
|
+
let isDragging = $state(false);
|
|
54
|
+
let animationsEnabled = $state(true);
|
|
55
|
+
let panOffset = $state({ x: 0, y: 0 });
|
|
56
|
+
let toolMode = $state('select');
|
|
57
|
+
const selectedAsset = $derived(scene.assets.find((a) => a.id === selectedAssetId) ?? null);
|
|
58
|
+
const assetCount = $derived(scene.assets.length);
|
|
59
|
+
const complexityUsage = $derived(Math.min(calculateSceneComplexity(scene.assets) / TERRARIUM_CONFIG.complexity.maxComplexity, 1));
|
|
60
|
+
const canAddAsset = $derived(calculateSceneComplexity(scene.assets) < TERRARIUM_CONFIG.complexity.maxComplexity);
|
|
61
|
+
function addAsset(componentName, category, position) {
|
|
62
|
+
// Guard: enforce complexity budget
|
|
63
|
+
if (!canAddAsset) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
// Validate position is within canvas bounds
|
|
67
|
+
const clampedPosition = {
|
|
68
|
+
x: Math.max(0, Math.min(position.x, scene.canvas.width)),
|
|
69
|
+
y: Math.max(0, Math.min(position.y, scene.canvas.height))
|
|
70
|
+
};
|
|
71
|
+
const id = crypto.randomUUID();
|
|
72
|
+
const maxZ = getMaxZIndex(scene.assets);
|
|
73
|
+
const newAsset = {
|
|
74
|
+
id,
|
|
75
|
+
componentName,
|
|
76
|
+
category,
|
|
77
|
+
position: clampedPosition,
|
|
78
|
+
scale: TERRARIUM_CONFIG.asset.defaultScale,
|
|
79
|
+
rotation: 0,
|
|
80
|
+
zIndex: maxZ + 1,
|
|
81
|
+
props: {},
|
|
82
|
+
animationEnabled: animationsEnabled
|
|
83
|
+
};
|
|
84
|
+
scene.assets.push(newAsset);
|
|
85
|
+
scene.updatedAt = new Date().toISOString();
|
|
86
|
+
selectedAssetId = id;
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
function updateAsset(id, updates) {
|
|
90
|
+
const index = scene.assets.findIndex((a) => a.id === id);
|
|
91
|
+
if (index === -1)
|
|
92
|
+
return;
|
|
93
|
+
scene.assets[index] = {
|
|
94
|
+
...scene.assets[index],
|
|
95
|
+
...updates,
|
|
96
|
+
id,
|
|
97
|
+
position: updates.position
|
|
98
|
+
? { ...updates.position }
|
|
99
|
+
: scene.assets[index].position,
|
|
100
|
+
props: updates.props ? { ...updates.props } : scene.assets[index].props
|
|
101
|
+
};
|
|
102
|
+
scene.updatedAt = new Date().toISOString();
|
|
103
|
+
}
|
|
104
|
+
function deleteAsset(id) {
|
|
105
|
+
const index = scene.assets.findIndex((a) => a.id === id);
|
|
106
|
+
if (index === -1)
|
|
107
|
+
return;
|
|
108
|
+
scene.assets.splice(index, 1);
|
|
109
|
+
scene.updatedAt = new Date().toISOString();
|
|
110
|
+
if (selectedAssetId === id) {
|
|
111
|
+
selectedAssetId = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function duplicateAsset(id) {
|
|
115
|
+
const asset = scene.assets.find((a) => a.id === id);
|
|
116
|
+
if (!asset)
|
|
117
|
+
return '';
|
|
118
|
+
const newId = crypto.randomUUID();
|
|
119
|
+
const maxZ = getMaxZIndex(scene.assets);
|
|
120
|
+
const offset = TERRARIUM_CONFIG.ui.duplicateOffset;
|
|
121
|
+
const duplicatedAsset = {
|
|
122
|
+
...asset,
|
|
123
|
+
id: newId,
|
|
124
|
+
position: {
|
|
125
|
+
x: asset.position.x + offset,
|
|
126
|
+
y: asset.position.y + offset
|
|
127
|
+
},
|
|
128
|
+
zIndex: maxZ + 1,
|
|
129
|
+
props: { ...asset.props }
|
|
130
|
+
};
|
|
131
|
+
scene.assets.push(duplicatedAsset);
|
|
132
|
+
scene.updatedAt = new Date().toISOString();
|
|
133
|
+
selectedAssetId = newId;
|
|
134
|
+
return newId;
|
|
135
|
+
}
|
|
136
|
+
function selectAsset(id) {
|
|
137
|
+
selectedAssetId = id;
|
|
138
|
+
}
|
|
139
|
+
function moveLayer(id, direction) {
|
|
140
|
+
const index = scene.assets.findIndex((a) => a.id === id);
|
|
141
|
+
if (index === -1)
|
|
142
|
+
return;
|
|
143
|
+
const asset = scene.assets[index];
|
|
144
|
+
const currentZ = asset.zIndex;
|
|
145
|
+
if (direction === 'top') {
|
|
146
|
+
const maxZ = getMaxZIndex(scene.assets);
|
|
147
|
+
if (currentZ === maxZ)
|
|
148
|
+
return;
|
|
149
|
+
asset.zIndex = maxZ + 1;
|
|
150
|
+
}
|
|
151
|
+
else if (direction === 'bottom') {
|
|
152
|
+
const minZ = Math.min(...scene.assets.map((a) => a.zIndex));
|
|
153
|
+
if (currentZ === minZ)
|
|
154
|
+
return;
|
|
155
|
+
asset.zIndex = minZ - 1;
|
|
156
|
+
}
|
|
157
|
+
else if (direction === 'up') {
|
|
158
|
+
const higherAssets = scene.assets.filter((a) => a.zIndex > currentZ);
|
|
159
|
+
if (higherAssets.length === 0)
|
|
160
|
+
return;
|
|
161
|
+
const nextZ = Math.min(...higherAssets.map((a) => a.zIndex));
|
|
162
|
+
const swapAsset = scene.assets.find((a) => a.zIndex === nextZ);
|
|
163
|
+
if (swapAsset) {
|
|
164
|
+
swapAsset.zIndex = currentZ;
|
|
165
|
+
asset.zIndex = nextZ;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (direction === 'down') {
|
|
169
|
+
const lowerAssets = scene.assets.filter((a) => a.zIndex < currentZ);
|
|
170
|
+
if (lowerAssets.length === 0)
|
|
171
|
+
return;
|
|
172
|
+
const prevZ = Math.max(...lowerAssets.map((a) => a.zIndex));
|
|
173
|
+
const swapAsset = scene.assets.find((a) => a.zIndex === prevZ);
|
|
174
|
+
if (swapAsset) {
|
|
175
|
+
swapAsset.zIndex = currentZ;
|
|
176
|
+
asset.zIndex = prevZ;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
scene.updatedAt = new Date().toISOString();
|
|
180
|
+
// Normalize z-indices periodically to prevent drift
|
|
181
|
+
normalizeZIndices();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Normalizes z-indices to sequential values (0, 1, 2, ...) to prevent
|
|
185
|
+
* accumulation of very large or negative values over time.
|
|
186
|
+
*/
|
|
187
|
+
function normalizeZIndices() {
|
|
188
|
+
if (scene.assets.length === 0)
|
|
189
|
+
return;
|
|
190
|
+
// Sort by current z-index, then reassign sequential values
|
|
191
|
+
const sorted = [...scene.assets].sort((a, b) => a.zIndex - b.zIndex);
|
|
192
|
+
sorted.forEach((asset, index) => {
|
|
193
|
+
const original = scene.assets.find((a) => a.id === asset.id);
|
|
194
|
+
if (original) {
|
|
195
|
+
original.zIndex = index;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function setScene(newScene) {
|
|
200
|
+
scene = {
|
|
201
|
+
...newScene,
|
|
202
|
+
canvas: { ...newScene.canvas },
|
|
203
|
+
assets: newScene.assets.map((asset) => ({
|
|
204
|
+
...asset,
|
|
205
|
+
position: { ...asset.position },
|
|
206
|
+
props: { ...asset.props }
|
|
207
|
+
}))
|
|
208
|
+
};
|
|
209
|
+
selectedAssetId = null;
|
|
210
|
+
panOffset = { x: 0, y: 0 };
|
|
211
|
+
// Normalize z-indices when loading external data
|
|
212
|
+
normalizeZIndices();
|
|
213
|
+
}
|
|
214
|
+
function resetScene() {
|
|
215
|
+
scene = createEmptyScene();
|
|
216
|
+
selectedAssetId = null;
|
|
217
|
+
panOffset = { x: 0, y: 0 };
|
|
218
|
+
}
|
|
219
|
+
function toggleAnimations() {
|
|
220
|
+
animationsEnabled = !animationsEnabled;
|
|
221
|
+
for (const asset of scene.assets) {
|
|
222
|
+
asset.animationEnabled = animationsEnabled;
|
|
223
|
+
}
|
|
224
|
+
scene.updatedAt = new Date().toISOString();
|
|
225
|
+
}
|
|
226
|
+
function toggleGrid() {
|
|
227
|
+
scene.canvas.gridEnabled = !scene.canvas.gridEnabled;
|
|
228
|
+
scene.updatedAt = new Date().toISOString();
|
|
229
|
+
}
|
|
230
|
+
function setGridSize(size) {
|
|
231
|
+
scene.canvas.gridSize = size;
|
|
232
|
+
scene.updatedAt = new Date().toISOString();
|
|
233
|
+
}
|
|
234
|
+
function setPanOffset(offset) {
|
|
235
|
+
panOffset = { ...offset };
|
|
236
|
+
}
|
|
237
|
+
function setToolMode(mode) {
|
|
238
|
+
toolMode = mode;
|
|
239
|
+
if (mode === 'pan') {
|
|
240
|
+
selectedAssetId = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
get scene() {
|
|
245
|
+
return scene;
|
|
246
|
+
},
|
|
247
|
+
get selectedAssetId() {
|
|
248
|
+
return selectedAssetId;
|
|
249
|
+
},
|
|
250
|
+
get isDragging() {
|
|
251
|
+
return isDragging;
|
|
252
|
+
},
|
|
253
|
+
set isDragging(value) {
|
|
254
|
+
isDragging = value;
|
|
255
|
+
},
|
|
256
|
+
get animationsEnabled() {
|
|
257
|
+
return animationsEnabled;
|
|
258
|
+
},
|
|
259
|
+
get panOffset() {
|
|
260
|
+
return panOffset;
|
|
261
|
+
},
|
|
262
|
+
get toolMode() {
|
|
263
|
+
return toolMode;
|
|
264
|
+
},
|
|
265
|
+
get selectedAsset() {
|
|
266
|
+
return selectedAsset;
|
|
267
|
+
},
|
|
268
|
+
get assetCount() {
|
|
269
|
+
return assetCount;
|
|
270
|
+
},
|
|
271
|
+
get canAddAsset() {
|
|
272
|
+
return canAddAsset;
|
|
273
|
+
},
|
|
274
|
+
get complexityUsage() {
|
|
275
|
+
return complexityUsage;
|
|
276
|
+
},
|
|
277
|
+
addAsset,
|
|
278
|
+
updateAsset,
|
|
279
|
+
deleteAsset,
|
|
280
|
+
duplicateAsset,
|
|
281
|
+
selectAsset,
|
|
282
|
+
moveLayer,
|
|
283
|
+
setScene,
|
|
284
|
+
resetScene,
|
|
285
|
+
toggleAnimations,
|
|
286
|
+
toggleGrid,
|
|
287
|
+
setGridSize,
|
|
288
|
+
setPanOffset,
|
|
289
|
+
setToolMode
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrarium Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Core types for the Terrarium creative canvas system.
|
|
5
|
+
*/
|
|
6
|
+
import type { Component as SvelteComponent } from 'svelte';
|
|
7
|
+
export type AssetCategory = 'trees' | 'creatures' | 'botanical' | 'ground' | 'sky' | 'structural' | 'water' | 'weather';
|
|
8
|
+
export interface Point {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Size {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
export interface CanvasSettings {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
background: string;
|
|
20
|
+
gridEnabled: boolean;
|
|
21
|
+
gridSize: 16 | 32 | 64;
|
|
22
|
+
}
|
|
23
|
+
export interface AssetMeta {
|
|
24
|
+
displayName: string;
|
|
25
|
+
category: AssetCategory;
|
|
26
|
+
isAnimated: boolean;
|
|
27
|
+
defaultSize: Size;
|
|
28
|
+
props: PropDefinition[];
|
|
29
|
+
}
|
|
30
|
+
export interface PropDefinition {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
type: 'number' | 'boolean' | 'string' | 'select' | 'color';
|
|
34
|
+
min?: number;
|
|
35
|
+
max?: number;
|
|
36
|
+
step?: number;
|
|
37
|
+
options?: {
|
|
38
|
+
value: string;
|
|
39
|
+
label: string;
|
|
40
|
+
}[];
|
|
41
|
+
default: unknown;
|
|
42
|
+
}
|
|
43
|
+
export interface AssetDefinition extends AssetMeta {
|
|
44
|
+
name: string;
|
|
45
|
+
load: () => Promise<{
|
|
46
|
+
default: SvelteComponent;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export interface PlacedAsset {
|
|
50
|
+
id: string;
|
|
51
|
+
componentName: string;
|
|
52
|
+
category: AssetCategory;
|
|
53
|
+
position: Point;
|
|
54
|
+
scale: number;
|
|
55
|
+
rotation: number;
|
|
56
|
+
zIndex: number;
|
|
57
|
+
props: Record<string, unknown>;
|
|
58
|
+
animationEnabled: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface TerrariumScene {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
version: 1;
|
|
64
|
+
canvas: CanvasSettings;
|
|
65
|
+
assets: PlacedAsset[];
|
|
66
|
+
createdAt: string;
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
}
|
|
69
|
+
export type DecorationZone = 'header' | 'sidebar' | 'footer' | 'background';
|
|
70
|
+
export interface DecorationOptions {
|
|
71
|
+
opacity: number;
|
|
72
|
+
}
|
|
73
|
+
export interface Decoration {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
zone: DecorationZone;
|
|
77
|
+
scene: TerrariumScene;
|
|
78
|
+
options: DecorationOptions;
|
|
79
|
+
thumbnail?: string;
|
|
80
|
+
authorId?: string;
|
|
81
|
+
isPublic: boolean;
|
|
82
|
+
createdAt: string;
|
|
83
|
+
}
|
|
84
|
+
export interface ExportOptions {
|
|
85
|
+
scale?: number;
|
|
86
|
+
backgroundColor?: string;
|
|
87
|
+
pauseAnimations?: boolean;
|
|
88
|
+
width?: number;
|
|
89
|
+
height?: number;
|
|
90
|
+
}
|
|
91
|
+
export interface DragState {
|
|
92
|
+
isDragging: boolean;
|
|
93
|
+
startPosition: Point;
|
|
94
|
+
currentPosition: Point;
|
|
95
|
+
assetId: string | null;
|
|
96
|
+
}
|
|
97
|
+
export interface PanState {
|
|
98
|
+
isPanning: boolean;
|
|
99
|
+
offset: Point;
|
|
100
|
+
startOffset: Point;
|
|
101
|
+
startMouse: Point;
|
|
102
|
+
}
|
|
103
|
+
export interface SelectionState {
|
|
104
|
+
selectedId: string | null;
|
|
105
|
+
isMultiSelect: boolean;
|
|
106
|
+
selectedIds: string[];
|
|
107
|
+
}
|
|
108
|
+
export type ToolMode = 'select' | 'pan' | 'place';
|
|
109
|
+
export interface ToolbarAction {
|
|
110
|
+
id: string;
|
|
111
|
+
label: string;
|
|
112
|
+
icon: string;
|
|
113
|
+
shortcut?: string;
|
|
114
|
+
action: () => void;
|
|
115
|
+
disabled?: boolean;
|
|
116
|
+
}
|
|
117
|
+
export declare const DEFAULT_SCENE: TerrariumScene;
|
|
118
|
+
export declare const CANVAS_BACKGROUNDS: readonly [{
|
|
119
|
+
readonly name: "Sky Gradient";
|
|
120
|
+
readonly value: "linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 50%, #A8E6CF 100%)";
|
|
121
|
+
}, {
|
|
122
|
+
readonly name: "Forest Dawn";
|
|
123
|
+
readonly value: "linear-gradient(to bottom, #FEF3C7 0%, #FDE68A 30%, #34D399 100%)";
|
|
124
|
+
}, {
|
|
125
|
+
readonly name: "Night Sky";
|
|
126
|
+
readonly value: "linear-gradient(to bottom, #1E1B4B 0%, #312E81 50%, #4C1D95 100%)";
|
|
127
|
+
}, {
|
|
128
|
+
readonly name: "Sunset";
|
|
129
|
+
readonly value: "linear-gradient(to bottom, #FDE68A 0%, #FB923C 40%, #EC4899 100%)";
|
|
130
|
+
}, {
|
|
131
|
+
readonly name: "Transparent";
|
|
132
|
+
readonly value: "transparent";
|
|
133
|
+
}, {
|
|
134
|
+
readonly name: "White";
|
|
135
|
+
readonly value: "#FFFFFF";
|
|
136
|
+
}, {
|
|
137
|
+
readonly name: "Cream";
|
|
138
|
+
readonly value: "#FFFBEB";
|
|
139
|
+
}];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrarium Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Core types for the Terrarium creative canvas system.
|
|
5
|
+
*/
|
|
6
|
+
// Default scene for initialization
|
|
7
|
+
export const DEFAULT_SCENE = {
|
|
8
|
+
id: '',
|
|
9
|
+
name: 'Untitled Scene',
|
|
10
|
+
version: 1,
|
|
11
|
+
canvas: {
|
|
12
|
+
width: 1200,
|
|
13
|
+
height: 800,
|
|
14
|
+
background: 'linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 50%, #A8E6CF 100%)',
|
|
15
|
+
gridEnabled: false,
|
|
16
|
+
gridSize: 32
|
|
17
|
+
},
|
|
18
|
+
assets: [],
|
|
19
|
+
createdAt: '',
|
|
20
|
+
updatedAt: ''
|
|
21
|
+
};
|
|
22
|
+
// Default canvas backgrounds
|
|
23
|
+
export const CANVAS_BACKGROUNDS = [
|
|
24
|
+
{
|
|
25
|
+
name: 'Sky Gradient',
|
|
26
|
+
value: 'linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 50%, #A8E6CF 100%)'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Forest Dawn',
|
|
30
|
+
value: 'linear-gradient(to bottom, #FEF3C7 0%, #FDE68A 30%, #34D399 100%)'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Night Sky',
|
|
34
|
+
value: 'linear-gradient(to bottom, #1E1B4B 0%, #312E81 50%, #4C1D95 100%)'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Sunset',
|
|
38
|
+
value: 'linear-gradient(to bottom, #FDE68A 0%, #FB923C 40%, #EC4899 100%)'
|
|
39
|
+
},
|
|
40
|
+
{ name: 'Transparent', value: 'transparent' },
|
|
41
|
+
{ name: 'White', value: '#FFFFFF' },
|
|
42
|
+
{ name: 'Cream', value: '#FFFBEB' }
|
|
43
|
+
];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrarium PNG Export Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles exporting Terrarium scenes as PNG images using dom-to-image-more.
|
|
5
|
+
* Supports pausing animations, custom scaling, and thumbnail generation.
|
|
6
|
+
*
|
|
7
|
+
* This file is part of Grove — A place to Be
|
|
8
|
+
* Copyright (c) 2025 Autumn Brown
|
|
9
|
+
*
|
|
10
|
+
* This program is free software: you can redistribute it and/or modify
|
|
11
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
12
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
13
|
+
* (at your option) any later version.
|
|
14
|
+
*
|
|
15
|
+
* This program is distributed in the hope that it will be useful,
|
|
16
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
18
|
+
* GNU Affero General Public License for more details.
|
|
19
|
+
*/
|
|
20
|
+
import type { ExportOptions } from '../types';
|
|
21
|
+
/**
|
|
22
|
+
* Sanitizes a filename by converting to lowercase and replacing
|
|
23
|
+
* non-alphanumeric characters with hyphens.
|
|
24
|
+
* Also truncates to a maximum length to prevent filesystem issues.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sanitizeFilename(name: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Triggers a download of a data URL with the specified filename.
|
|
29
|
+
*/
|
|
30
|
+
export declare function downloadDataUrl(dataUrl: string, filename: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Generates a PNG data URL from a canvas element.
|
|
33
|
+
* Useful for creating thumbnails or previews.
|
|
34
|
+
*
|
|
35
|
+
* @param canvasElement - The HTML element to capture
|
|
36
|
+
* @param options - Export configuration options
|
|
37
|
+
* @returns Promise resolving to a data URL string
|
|
38
|
+
*/
|
|
39
|
+
export declare function generateDataUrl(canvasElement: HTMLElement, options?: ExportOptions): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Exports a Terrarium scene as a PNG file.
|
|
42
|
+
* Pauses animations during capture and triggers a download.
|
|
43
|
+
*
|
|
44
|
+
* @param canvasElement - The HTML element to capture
|
|
45
|
+
* @param sceneName - Name of the scene (used for filename)
|
|
46
|
+
* @param options - Export configuration options
|
|
47
|
+
*/
|
|
48
|
+
export declare function exportSceneAsPNG(canvasElement: HTMLElement, sceneName: string, options?: ExportOptions): Promise<void>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrarium PNG Export Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles exporting Terrarium scenes as PNG images using dom-to-image-more.
|
|
5
|
+
* Supports pausing animations, custom scaling, and thumbnail generation.
|
|
6
|
+
*
|
|
7
|
+
* This file is part of Grove — A place to Be
|
|
8
|
+
* Copyright (c) 2025 Autumn Brown
|
|
9
|
+
*
|
|
10
|
+
* This program is free software: you can redistribute it and/or modify
|
|
11
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
12
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
13
|
+
* (at your option) any later version.
|
|
14
|
+
*
|
|
15
|
+
* This program is distributed in the hope that it will be useful,
|
|
16
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
18
|
+
* GNU Affero General Public License for more details.
|
|
19
|
+
*/
|
|
20
|
+
import domtoimage from 'dom-to-image-more';
|
|
21
|
+
import { TERRARIUM_CONFIG } from '../../../../config/terrarium';
|
|
22
|
+
/**
|
|
23
|
+
* Pauses all CSS animations and transitions on an element and its children.
|
|
24
|
+
* Returns a cleanup function to restore animations.
|
|
25
|
+
*/
|
|
26
|
+
function pauseAnimations(element) {
|
|
27
|
+
const originalStyles = new Map();
|
|
28
|
+
const allElements = [element, ...Array.from(element.querySelectorAll('*'))];
|
|
29
|
+
allElements.forEach((el) => {
|
|
30
|
+
const style = el.style.cssText;
|
|
31
|
+
originalStyles.set(el, style);
|
|
32
|
+
el.style.cssText += '; animation-play-state: paused !important; transition: none !important;';
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
originalStyles.forEach((style, el) => {
|
|
36
|
+
el.style.cssText = style;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Waits for a specified number of milliseconds.
|
|
42
|
+
*/
|
|
43
|
+
function wait(ms) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verifies that animations are actually paused on an element and all its children.
|
|
48
|
+
* Uses getComputedStyle to check animation-play-state.
|
|
49
|
+
*/
|
|
50
|
+
function verifyAnimationsPaused(element) {
|
|
51
|
+
const allElements = [element, ...Array.from(element.querySelectorAll('*'))];
|
|
52
|
+
return allElements.every((el) => {
|
|
53
|
+
const style = getComputedStyle(el);
|
|
54
|
+
return style.animationPlayState === 'paused' || style.animationPlayState === '';
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sanitizes a filename by converting to lowercase and replacing
|
|
59
|
+
* non-alphanumeric characters with hyphens.
|
|
60
|
+
* Also truncates to a maximum length to prevent filesystem issues.
|
|
61
|
+
*/
|
|
62
|
+
export function sanitizeFilename(name) {
|
|
63
|
+
const maxLength = TERRARIUM_CONFIG.ui.filenameMaxLength;
|
|
64
|
+
const sanitized = name
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
67
|
+
.replace(/-+/g, '-')
|
|
68
|
+
.replace(/^-|-$/g, '');
|
|
69
|
+
// Truncate to max length, ensuring we don't cut in the middle of a word
|
|
70
|
+
if (sanitized.length > maxLength) {
|
|
71
|
+
const truncated = sanitized.slice(0, maxLength);
|
|
72
|
+
// Find last hyphen to avoid cutting words
|
|
73
|
+
const lastHyphen = truncated.lastIndexOf('-');
|
|
74
|
+
return lastHyphen > maxLength / 2 ? truncated.slice(0, lastHyphen) : truncated;
|
|
75
|
+
}
|
|
76
|
+
return sanitized;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Triggers a download of a data URL with the specified filename.
|
|
80
|
+
*/
|
|
81
|
+
export function downloadDataUrl(dataUrl, filename) {
|
|
82
|
+
const anchor = document.createElement('a');
|
|
83
|
+
anchor.href = dataUrl;
|
|
84
|
+
anchor.download = filename;
|
|
85
|
+
anchor.style.display = 'none';
|
|
86
|
+
document.body.appendChild(anchor);
|
|
87
|
+
anchor.click();
|
|
88
|
+
document.body.removeChild(anchor);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Generates a PNG data URL from a canvas element.
|
|
92
|
+
* Useful for creating thumbnails or previews.
|
|
93
|
+
*
|
|
94
|
+
* @param canvasElement - The HTML element to capture
|
|
95
|
+
* @param options - Export configuration options
|
|
96
|
+
* @returns Promise resolving to a data URL string
|
|
97
|
+
*/
|
|
98
|
+
export async function generateDataUrl(canvasElement, options = {}) {
|
|
99
|
+
const { scale = TERRARIUM_CONFIG.export.defaultScale, backgroundColor, pauseAnimations: shouldPauseAnimations = true, width, height } = options;
|
|
100
|
+
let restoreAnimations = null;
|
|
101
|
+
try {
|
|
102
|
+
if (shouldPauseAnimations) {
|
|
103
|
+
restoreAnimations = pauseAnimations(canvasElement);
|
|
104
|
+
// Wait for animations to pause, using configurable timing
|
|
105
|
+
// Then verify they're actually paused before proceeding
|
|
106
|
+
await wait(TERRARIUM_CONFIG.ui.exportWaitMs);
|
|
107
|
+
if (!verifyAnimationsPaused(canvasElement)) {
|
|
108
|
+
// If still not paused, wait a bit longer for slower devices
|
|
109
|
+
await wait(TERRARIUM_CONFIG.ui.exportWaitMs);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const domToImageOptions = {
|
|
113
|
+
quality: 1.0
|
|
114
|
+
};
|
|
115
|
+
if (width !== undefined) {
|
|
116
|
+
domToImageOptions.width = width * scale;
|
|
117
|
+
}
|
|
118
|
+
if (height !== undefined) {
|
|
119
|
+
domToImageOptions.height = height * scale;
|
|
120
|
+
}
|
|
121
|
+
if (backgroundColor !== undefined) {
|
|
122
|
+
domToImageOptions.style = {
|
|
123
|
+
backgroundColor
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const dataUrl = await domtoimage.toPng(canvasElement, domToImageOptions);
|
|
127
|
+
return dataUrl;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
if (restoreAnimations) {
|
|
131
|
+
restoreAnimations();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Exports a Terrarium scene as a PNG file.
|
|
137
|
+
* Pauses animations during capture and triggers a download.
|
|
138
|
+
*
|
|
139
|
+
* @param canvasElement - The HTML element to capture
|
|
140
|
+
* @param sceneName - Name of the scene (used for filename)
|
|
141
|
+
* @param options - Export configuration options
|
|
142
|
+
*/
|
|
143
|
+
export async function exportSceneAsPNG(canvasElement, sceneName, options = {}) {
|
|
144
|
+
const dataUrl = await generateDataUrl(canvasElement, options);
|
|
145
|
+
const sanitizedName = sanitizeFilename(sceneName);
|
|
146
|
+
const filename = `${sanitizedName || 'terrarium-scene'}.png`;
|
|
147
|
+
downloadDataUrl(dataUrl, filename);
|
|
148
|
+
}
|
|
@@ -2,18 +2,8 @@ export { default as FontProvider } from './FontProvider.svelte';
|
|
|
2
2
|
export { default as Lexend } from './Lexend.svelte';
|
|
3
3
|
export { default as Atkinson } from './Atkinson.svelte';
|
|
4
4
|
export { default as OpenDyslexic } from './OpenDyslexic.svelte';
|
|
5
|
-
export { default as Luciole } from './Luciole.svelte';
|
|
6
|
-
export { default as Nunito } from './Nunito.svelte';
|
|
7
5
|
export { default as Quicksand } from './Quicksand.svelte';
|
|
8
|
-
export { default as Manrope } from './Manrope.svelte';
|
|
9
|
-
export { default as InstrumentSans } from './InstrumentSans.svelte';
|
|
10
6
|
export { default as PlusJakartaSans } from './PlusJakartaSans.svelte';
|
|
11
|
-
export { default as Cormorant } from './Cormorant.svelte';
|
|
12
|
-
export { default as BodoniModa } from './BodoniModa.svelte';
|
|
13
|
-
export { default as Lora } from './Lora.svelte';
|
|
14
|
-
export { default as EBGaramond } from './EBGaramond.svelte';
|
|
15
|
-
export { default as Merriweather } from './Merriweather.svelte';
|
|
16
|
-
export { default as Fraunces } from './Fraunces.svelte';
|
|
17
7
|
export { default as IBMPlexMono } from './IBMPlexMono.svelte';
|
|
18
8
|
export { default as Cozette } from './Cozette.svelte';
|
|
19
9
|
export { default as Alagard } from './Alagard.svelte';
|