sliccy 1.18.2 → 1.19.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.
Files changed (96) hide show
  1. package/dist/ui/assets/{___vite-browser-external_commonjs-proxy-BwJgphaY.js → ___vite-browser-external_commonjs-proxy-DU9g-pAk.js} +1 -1
  2. package/dist/ui/assets/{bsh-watchdog-qD-Teoo4.js → bsh-watchdog-D8cxZoGx.js} +1 -1
  3. package/dist/ui/assets/{index-DZrFxY8C.js → index-BMrWjOA6.js} +1 -1
  4. package/dist/ui/assets/{index-fbqdhDlF.js → index-BjvBFNU_.js} +1 -1
  5. package/dist/ui/assets/{index-CL0Bwyen.js → index-BwyPKnK5.js} +289 -289
  6. package/dist/ui/assets/{index-BvZFUJlY.js → index-CJGeHOsM.js} +1 -1
  7. package/dist/ui/assets/{index-KFWCKVJQ.js → index-CQZA7VKt.js} +1 -1
  8. package/dist/ui/assets/{index-B8IwKAYd.js → index-CSm6gBcH.js} +1 -1
  9. package/dist/ui/assets/{index-BAVxG_Su.js → index-DeHghjQD.js} +1 -1
  10. package/dist/ui/assets/{index-JXeYDsaX.js → index-DiiWjd3G.js} +1 -1
  11. package/dist/ui/assets/{index-BsoP4arC.js → index-HQcZZzOy.js} +1 -1
  12. package/dist/ui/assets/{offscreen-client-CFibtznc.js → offscreen-client-ClelxYjR.js} +1 -1
  13. package/dist/ui/assets/{sql-wasm-DD4iv2bM.js → sql-wasm-BQajMxK_.js} +1 -1
  14. package/dist/ui/electron-overlay-entry.js +162 -32
  15. package/dist/ui/index.html +2 -2
  16. package/dist/ui/logos/Sliccy Logo.svg +32 -0
  17. package/dist/ui/logos/logo-editor.html +879 -0
  18. package/dist/ui/logos/sliccy-color-0scoops-128x128.png +0 -0
  19. package/dist/ui/logos/sliccy-color-0scoops-16x16.png +0 -0
  20. package/dist/ui/logos/sliccy-color-0scoops-32x32.png +0 -0
  21. package/dist/ui/logos/sliccy-color-0scoops-48x48.png +0 -0
  22. package/dist/ui/logos/sliccy-color-0scoops.svg +27 -0
  23. package/dist/ui/logos/sliccy-color-10scoops-128x128.png +0 -0
  24. package/dist/ui/logos/sliccy-color-10scoops-16x16.png +0 -0
  25. package/dist/ui/logos/sliccy-color-10scoops-32x32.png +0 -0
  26. package/dist/ui/logos/sliccy-color-10scoops-48x48.png +0 -0
  27. package/dist/ui/logos/sliccy-color-10scoops.svg +27 -0
  28. package/dist/ui/logos/sliccy-color-1scoops-128x128.png +0 -0
  29. package/dist/ui/logos/sliccy-color-1scoops-16x16.png +0 -0
  30. package/dist/ui/logos/sliccy-color-1scoops-32x32.png +0 -0
  31. package/dist/ui/logos/sliccy-color-1scoops-48x48.png +0 -0
  32. package/dist/ui/logos/sliccy-color-1scoops.svg +27 -0
  33. package/dist/ui/logos/sliccy-color-2scoops-128x128.png +0 -0
  34. package/dist/ui/logos/sliccy-color-2scoops-16x16.png +0 -0
  35. package/dist/ui/logos/sliccy-color-2scoops-32x32.png +0 -0
  36. package/dist/ui/logos/sliccy-color-2scoops-48x48.png +0 -0
  37. package/dist/ui/logos/sliccy-color-2scoops.svg +27 -0
  38. package/dist/ui/logos/sliccy-color-3scoops-128x128.png +0 -0
  39. package/dist/ui/logos/sliccy-color-3scoops-16x16.png +0 -0
  40. package/dist/ui/logos/sliccy-color-3scoops-32x32.png +0 -0
  41. package/dist/ui/logos/sliccy-color-3scoops-48x48.png +0 -0
  42. package/dist/ui/logos/sliccy-color-3scoops.svg +27 -0
  43. package/dist/ui/logos/sliccy-color-4scoops-128x128.png +0 -0
  44. package/dist/ui/logos/sliccy-color-4scoops-16x16.png +0 -0
  45. package/dist/ui/logos/sliccy-color-4scoops-32x32.png +0 -0
  46. package/dist/ui/logos/sliccy-color-4scoops-48x48.png +0 -0
  47. package/dist/ui/logos/sliccy-color-4scoops.svg +27 -0
  48. package/dist/ui/logos/sliccy-color-5scoops-128x128.png +0 -0
  49. package/dist/ui/logos/sliccy-color-5scoops-16x16.png +0 -0
  50. package/dist/ui/logos/sliccy-color-5scoops-32x32.png +0 -0
  51. package/dist/ui/logos/sliccy-color-5scoops-48x48.png +0 -0
  52. package/dist/ui/logos/sliccy-color-5scoops.svg +27 -0
  53. package/dist/ui/logos/sliccy-color-6scoops-128x128.png +0 -0
  54. package/dist/ui/logos/sliccy-color-6scoops-16x16.png +0 -0
  55. package/dist/ui/logos/sliccy-color-6scoops-32x32.png +0 -0
  56. package/dist/ui/logos/sliccy-color-6scoops-48x48.png +0 -0
  57. package/dist/ui/logos/sliccy-color-6scoops.svg +27 -0
  58. package/dist/ui/logos/sliccy-color-7scoops-128x128.png +0 -0
  59. package/dist/ui/logos/sliccy-color-7scoops-16x16.png +0 -0
  60. package/dist/ui/logos/sliccy-color-7scoops-32x32.png +0 -0
  61. package/dist/ui/logos/sliccy-color-7scoops-48x48.png +0 -0
  62. package/dist/ui/logos/sliccy-color-7scoops.svg +27 -0
  63. package/dist/ui/logos/sliccy-color-8scoops-128x128.png +0 -0
  64. package/dist/ui/logos/sliccy-color-8scoops-16x16.png +0 -0
  65. package/dist/ui/logos/sliccy-color-8scoops-32x32.png +0 -0
  66. package/dist/ui/logos/sliccy-color-8scoops-48x48.png +0 -0
  67. package/dist/ui/logos/sliccy-color-8scoops.svg +27 -0
  68. package/dist/ui/logos/sliccy-color-9scoops-128x128.png +0 -0
  69. package/dist/ui/logos/sliccy-color-9scoops-16x16.png +0 -0
  70. package/dist/ui/logos/sliccy-color-9scoops-32x32.png +0 -0
  71. package/dist/ui/logos/sliccy-color-9scoops-48x48.png +0 -0
  72. package/dist/ui/logos/sliccy-color-9scoops.svg +27 -0
  73. package/dist/ui/logos/sliccy-mono-dark-0scoops.svg +27 -0
  74. package/dist/ui/logos/sliccy-mono-dark-10scoops.svg +27 -0
  75. package/dist/ui/logos/sliccy-mono-dark-1scoops.svg +27 -0
  76. package/dist/ui/logos/sliccy-mono-dark-2scoops.svg +27 -0
  77. package/dist/ui/logos/sliccy-mono-dark-3scoops.svg +27 -0
  78. package/dist/ui/logos/sliccy-mono-dark-4scoops.svg +27 -0
  79. package/dist/ui/logos/sliccy-mono-dark-5scoops.svg +27 -0
  80. package/dist/ui/logos/sliccy-mono-dark-6scoops.svg +27 -0
  81. package/dist/ui/logos/sliccy-mono-dark-7scoops.svg +27 -0
  82. package/dist/ui/logos/sliccy-mono-dark-8scoops.svg +27 -0
  83. package/dist/ui/logos/sliccy-mono-dark-9scoops.svg +27 -0
  84. package/dist/ui/logos/sliccy-mono-light-0scoops.svg +27 -0
  85. package/dist/ui/logos/sliccy-mono-light-10scoops.svg +27 -0
  86. package/dist/ui/logos/sliccy-mono-light-1scoops.svg +27 -0
  87. package/dist/ui/logos/sliccy-mono-light-2scoops.svg +27 -0
  88. package/dist/ui/logos/sliccy-mono-light-3scoops.svg +27 -0
  89. package/dist/ui/logos/sliccy-mono-light-4scoops.svg +27 -0
  90. package/dist/ui/logos/sliccy-mono-light-5scoops.svg +27 -0
  91. package/dist/ui/logos/sliccy-mono-light-6scoops.svg +27 -0
  92. package/dist/ui/logos/sliccy-mono-light-7scoops.svg +27 -0
  93. package/dist/ui/logos/sliccy-mono-light-8scoops.svg +27 -0
  94. package/dist/ui/logos/sliccy-mono-light-9scoops.svg +27 -0
  95. package/dist/ui/packages/webapp/index.html +2 -2
  96. package/package.json +1 -1
@@ -0,0 +1,879 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sliccy Logo Editor</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ display: flex;
12
+ min-height: 100vh;
13
+ transition: background-color 0.3s, color 0.3s;
14
+ }
15
+ body.light { background: #f5f5f5; color: #333; }
16
+ body.dark { background: #1a1a1a; color: #eee; }
17
+
18
+ .controls {
19
+ width: 320px;
20
+ padding: 20px;
21
+ overflow-y: auto;
22
+ border-right: 1px solid #ccc;
23
+ }
24
+ body.dark .controls { border-color: #444; }
25
+
26
+ .preview {
27
+ flex: 1;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ padding: 20px;
32
+ }
33
+
34
+ .logo-variants {
35
+ display: flex;
36
+ gap: 20px;
37
+ align-items: flex-start;
38
+ }
39
+ .logo-variant {
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ }
44
+ .logo-variant-label {
45
+ font-size: 0.85em;
46
+ margin-bottom: 8px;
47
+ opacity: 0.7;
48
+ }
49
+ .logo-container {
50
+ width: 300px;
51
+ height: 300px;
52
+ position: relative;
53
+ border: 1px dashed #ccc;
54
+ }
55
+ body.dark .logo-container {
56
+ border-color: #444;
57
+ }
58
+ .logo-container svg {
59
+ width: 100%;
60
+ height: 100%;
61
+ }
62
+
63
+ h1 { font-size: 1.4em; margin-bottom: 20px; }
64
+ h2 { font-size: 1em; margin: 20px 0 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px; }
65
+ body.dark h2 { border-color: #444; }
66
+
67
+ .control-group { margin-bottom: 15px; }
68
+ label { display: block; margin-bottom: 5px; font-size: 0.9em; }
69
+ input[type="range"] { width: 100%; }
70
+ input[type="number"] { width: 60px; padding: 4px; }
71
+ select {
72
+ width: 100%;
73
+ padding: 6px;
74
+ border-radius: 4px;
75
+ border: 1px solid #ccc;
76
+ background: #fff;
77
+ color: #333;
78
+ font-size: 0.9em;
79
+ }
80
+ body.dark select {
81
+ background: #333;
82
+ color: #eee;
83
+ border-color: #555;
84
+ }
85
+
86
+ .toggle-btn {
87
+ padding: 8px 16px;
88
+ border: none;
89
+ border-radius: 4px;
90
+ cursor: pointer;
91
+ font-size: 0.9em;
92
+ margin-right: 8px;
93
+ }
94
+ body.light .toggle-btn { background: #333; color: #fff; }
95
+ body.dark .toggle-btn { background: #fff; color: #333; }
96
+
97
+ .value-display {
98
+ font-size: 0.85em;
99
+ color: #666;
100
+ margin-left: 8px;
101
+ }
102
+ body.dark .value-display { color: #aaa; }
103
+
104
+ /* Eye animations */
105
+ @keyframes eyeroll {
106
+ 0%, 100% { transform: translate(0, 0); }
107
+ 25% { transform: translate(-15px, -10px); }
108
+ 50% { transform: translate(0, -15px); }
109
+ 75% { transform: translate(15px, -10px); }
110
+ }
111
+
112
+ .pupil-animated {
113
+ animation-iteration-count: infinite;
114
+ animation-timing-function: ease-in-out;
115
+ }
116
+ </style>
117
+ </head>
118
+ <body class="light">
119
+ <div class="controls">
120
+ <h1>🍦 Sliccy Logo Editor</h1>
121
+
122
+ <div class="control-group">
123
+ <label>Color Mode:</label>
124
+ <select id="color-mode">
125
+ <option value="mono-light">Monochrome Light</option>
126
+ <option value="mono-dark">Monochrome Dark</option>
127
+ <option value="color">Color</option>
128
+ </select>
129
+ </div>
130
+
131
+ <h2>Scoops</h2>
132
+ <div class="control-group">
133
+ <label>Number of Scoops: <span class="value-display" id="scoop-count-val">5</span></label>
134
+ <input type="range" id="scoop-count" min="0" max="10" value="5">
135
+ </div>
136
+
137
+ <h2>Scoop Stacking</h2>
138
+ <div class="control-group">
139
+ <label>Vertical Offset per Scoop: <span class="value-display" id="scoop-offset-val">95</span></label>
140
+ <input type="range" id="scoop-offset" min="50" max="200" value="95">
141
+ </div>
142
+ <div class="control-group">
143
+ <label>Scale per Additional Scoop: <span class="value-display" id="scoop-scale-val">0.95</span></label>
144
+ <input type="range" id="scoop-scale" min="0.5" max="1" step="0.01" value="0.95">
145
+ </div>
146
+
147
+ <h2>Mid-Range Adjustments (1-9 Scoops)</h2>
148
+ <div class="control-group">
149
+ <label>Mid-Range Scale Factor: <span class="value-display" id="midrange-scale-val">1.0</span></label>
150
+ <input type="range" id="midrange-scale" min="0.8" max="1.2" step="0.01" value="1.0">
151
+ </div>
152
+ <div class="control-group">
153
+ <label>Mid-Range Y Offset: <span class="value-display" id="midrange-y-val">0</span></label>
154
+ <input type="range" id="midrange-y" min="-200" max="200" value="0">
155
+ </div>
156
+ <div class="control-group">
157
+ <label>Horizontal Wobble: <span class="value-display" id="scoop-wobble-val">14</span></label>
158
+ <input type="range" id="scoop-wobble" min="0" max="50" value="14">
159
+ </div>
160
+ <div class="control-group">
161
+ <label>Angular Wobble (°): <span class="value-display" id="angular-wobble-val">8</span></label>
162
+ <input type="range" id="angular-wobble" min="0" max="30" value="8">
163
+ </div>
164
+ <div class="control-group">
165
+ <button class="toggle-btn" id="randomize-wobble">🎲 Randomize Wobble</button>
166
+ </div>
167
+ <div class="control-group">
168
+ <label>Cone Push-Down per Scoop: <span class="value-display" id="cone-push-val">-21</span></label>
169
+ <input type="range" id="cone-push" min="-100" max="0" value="-21">
170
+ </div>
171
+
172
+ <h2>Eye Animations</h2>
173
+ <div class="control-group">
174
+ <label>
175
+ <input type="checkbox" id="eye-animate"> Enable Eye Animations
176
+ </label>
177
+ </div>
178
+ <div class="control-group">
179
+ <label>Eyeroll Speed (s): <span class="value-display" id="eyeroll-speed-val">3</span></label>
180
+ <input type="range" id="eyeroll-speed" min="0.5" max="10" step="0.5" value="3">
181
+ </div>
182
+ <div class="control-group">
183
+ <label>Eye Distance: <span class="value-display" id="eye-distance-val">1.0</span></label>
184
+ <input type="range" id="eye-distance" min="0.5" max="1.5" step="0.05" value="1.0">
185
+ </div>
186
+ <div class="control-group">
187
+ <label>Eye Size: <span class="value-display" id="eye-size-val">1.0</span></label>
188
+ <input type="range" id="eye-size" min="0.5" max="2" step="0.05" value="1.0">
189
+ </div>
190
+ <div class="control-group">
191
+ <label>Eye Angle (°): <span class="value-display" id="eye-angle-val">0</span></label>
192
+ <input type="range" id="eye-angle" min="-45" max="45" value="0">
193
+ </div>
194
+
195
+ <h2>Scoop Colors</h2>
196
+ <div id="color-controls-info" style="font-size: 0.85em; opacity: 0.7; margin-bottom: 10px;">
197
+ Only visible in Color mode
198
+ </div>
199
+ <div id="color-controls"></div>
200
+
201
+ <h2>Export</h2>
202
+ <div class="control-group">
203
+ <button class="toggle-btn" id="generate-variants">Generate All Variants</button>
204
+ </div>
205
+ <div id="export-status" style="font-size: 0.85em; margin-top: 10px;"></div>
206
+ </div>
207
+
208
+ <div class="preview">
209
+ <div class="logo-variants">
210
+ <div class="logo-variant">
211
+ <div class="logo-variant-label">No Scoops</div>
212
+ <div class="logo-container" id="logo-container-0"></div>
213
+ </div>
214
+ <div class="logo-variant">
215
+ <div class="logo-variant-label" id="label-user">5 Scoops</div>
216
+ <div class="logo-container" id="logo-container-user"></div>
217
+ </div>
218
+ <div class="logo-variant">
219
+ <div class="logo-variant-label">10 Scoops</div>
220
+ <div class="logo-container" id="logo-container-10"></div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <script>
226
+ let svgDoc = null;
227
+ let scoopTemplate = null;
228
+ let coneTemplate = null;
229
+ let wobbleSeeds = []; // Random seeds for consistent wobble per scoop
230
+
231
+ // Ice cream color palette with base colors and darker outline colors
232
+ const scoopColors = [
233
+ { base: '#FFB6C1', dark: '#E89AAB', name: 'Strawberry' }, // Pink
234
+ { base: '#98FB98', dark: '#7FD87F', name: 'Mint' }, // Mint green
235
+ { base: '#87CEEB', dark: '#6BB6D8', name: 'Blueberry' }, // Sky blue
236
+ { base: '#DDA0DD', dark: '#C77DC7', name: 'Grape' }, // Plum
237
+ { base: '#F0E68C', dark: '#D4C970', name: 'Vanilla' }, // Khaki
238
+ { base: '#FFD700', dark: '#E6C200', name: 'Mango' }, // Gold
239
+ { base: '#FFA07A', dark: '#E88A64', name: 'Peach' }, // Light salmon
240
+ { base: '#DEB887', dark: '#C8A277', name: 'Caramel' }, // Burlywood
241
+ { base: '#F08080', dark: '#D86A6A', name: 'Cherry' }, // Light coral
242
+ { base: '#E0BBE4', dark: '#C9A5CD', name: 'Lavender' } // Lavender
243
+ ];
244
+
245
+ // Cone colors
246
+ const coneColors = {
247
+ base: '#D2691E', // Golden brown (chocolate)
248
+ crosshatch: '#E8A75C', // Lighter brown
249
+ outline: '#8B4513' // Saddle brown (darker)
250
+ };
251
+
252
+ // Generate random wobble seeds for scoops
253
+ function regenerateWobbleSeeds(count) {
254
+ wobbleSeeds = [];
255
+ for (let i = 0; i < count; i++) {
256
+ wobbleSeeds.push({
257
+ x: Math.random() * 2 - 1, // -1 to 1
258
+ angle: Math.random() * 2 - 1 // -1 to 1
259
+ });
260
+ }
261
+ }
262
+
263
+ async function loadSVG() {
264
+ const response = await fetch('Sliccy Logo.svg');
265
+ const text = await response.text();
266
+ const parser = new DOMParser();
267
+ svgDoc = parser.parseFromString(text, 'image/svg+xml');
268
+
269
+ scoopTemplate = {
270
+ outline: svgDoc.getElementById('Scoop_Outline').cloneNode(true),
271
+ rightEye: svgDoc.getElementById('Right_Eye').cloneNode(true),
272
+ leftEye: svgDoc.getElementById('Left_Eye').cloneNode(true)
273
+ };
274
+ coneTemplate = svgDoc.getElementById('Cone').cloneNode(true);
275
+
276
+ generateColorControls();
277
+ render();
278
+ }
279
+
280
+ function generateColorControls() {
281
+ const container = document.getElementById('color-controls');
282
+ container.innerHTML = '';
283
+ const count = parseInt(document.getElementById('scoop-count').value);
284
+ const colorMode = document.getElementById('color-mode').value;
285
+
286
+ // Only show color pickers in color mode
287
+ if (colorMode !== 'color') {
288
+ return;
289
+ }
290
+
291
+ for (let i = 0; i < count; i++) {
292
+ const div = document.createElement('div');
293
+ div.className = 'control-group';
294
+ const colorData = scoopColors[i % scoopColors.length];
295
+ div.innerHTML = `
296
+ <label>Scoop ${i + 1} (${colorData.name}):</label>
297
+ <input type="color" id="scoop-color-${i}" value="${colorData.base}">
298
+ `;
299
+ container.appendChild(div);
300
+ document.getElementById(`scoop-color-${i}`).addEventListener('input', render);
301
+ }
302
+ }
303
+
304
+ function renderLogo(container, count, params) {
305
+ const { offset, scaleDecay, wobble, angularWobble, conePush, colorMode, midrangeScale, midrangeY } = params;
306
+
307
+ // Linear interpolation based on scoop count (0-10)
308
+ // For 0 scoops: baseY = -175, scale = 1.25
309
+ // For 10 scoops: baseY = 512, scale = 1.1
310
+ const t = count / 10;
311
+ let baseY = -175 + t * (512 - (-175)); // -175 to 512
312
+ let overallScale = 1.25 + t * (1.1 - 1.25); // 1.25 to 1.1
313
+
314
+ // Apply mid-range adjustments for 1-9 scoops using calibrated curves
315
+ if (count >= 1 && count <= 9) {
316
+ // Scale factor: interpolate from 0.9 (at 1-2) to 1.0 (at 7-9)
317
+ // Using piecewise linear: 0.9 at 1-2, 0.95 at 5, 1.0 at 7+
318
+ let scaleFactor;
319
+ if (count <= 2) {
320
+ scaleFactor = 0.9;
321
+ } else if (count <= 5) {
322
+ // Linear from 0.9 to 0.95 between scoops 2 and 5
323
+ scaleFactor = 0.9 + (count - 2) * (0.95 - 0.9) / (5 - 2);
324
+ } else {
325
+ // Linear from 0.95 to 1.0 between scoops 5 and 7
326
+ scaleFactor = 0.95 + Math.min(count - 5, 2) * (1.0 - 0.95) / (7 - 5);
327
+ }
328
+
329
+ // Y offset: peaks at 120 (scoops 2-5), then decreases to 50 (scoop 9)
330
+ // 1: 100, 2-5: 120, 7: 110, 8: 80, 9: 50
331
+ let yOffset;
332
+ if (count === 1) {
333
+ yOffset = 100;
334
+ } else if (count <= 5) {
335
+ yOffset = 120;
336
+ } else if (count === 6) {
337
+ yOffset = 115; // interpolate between 120 and 110
338
+ } else if (count === 7) {
339
+ yOffset = 110;
340
+ } else if (count === 8) {
341
+ yOffset = 80;
342
+ } else { // count === 9
343
+ yOffset = 50;
344
+ }
345
+
346
+ overallScale *= scaleFactor * midrangeScale;
347
+ baseY += yOffset + midrangeY;
348
+ }
349
+
350
+ // Ensure we have enough wobble seeds
351
+ while (wobbleSeeds.length < count) {
352
+ wobbleSeeds.push({
353
+ x: Math.random() * 2 - 1,
354
+ angle: Math.random() * 2 - 1
355
+ });
356
+ }
357
+
358
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
359
+ // Use square viewBox (1024x1024) and center the content horizontally
360
+ const viewBoxWidth = 1024;
361
+ const viewBoxHeight = 1024;
362
+ const contentWidth = 576.75;
363
+ const xOffset = (viewBoxWidth - contentWidth) / 2;
364
+ svg.setAttribute('viewBox', `0 0 ${viewBoxWidth} ${viewBoxHeight}`);
365
+ svg.setAttribute('width', '100%');
366
+ svg.setAttribute('height', '100%');
367
+
368
+ const mainGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
369
+ const conePushTotal = (count - 1) * conePush;
370
+ const totalHeight = 887.3 + (count - 1) * offset + conePushTotal;
371
+ const scaleFactor = Math.min(1, 900 / totalHeight) * overallScale;
372
+ const translateY = (viewBoxHeight - totalHeight * scaleFactor) / 2 + baseY;
373
+ const translateX = xOffset + (contentWidth - contentWidth * scaleFactor) / 2;
374
+ mainGroup.setAttribute('transform', `translate(${translateX}, ${translateY}) scale(${scaleFactor})`);
375
+
376
+ // Add cone (pushed down by additional scoops)
377
+ const coneGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
378
+ coneGroup.setAttribute('transform', `translate(0, ${conePushTotal})`);
379
+ const cone = coneTemplate.cloneNode(true);
380
+ applyConeColors(cone, colorMode);
381
+ coneGroup.appendChild(cone);
382
+
383
+ // If no scoops, add eyes to the cone
384
+ if (count === 0) {
385
+ const rightEye = scoopTemplate.rightEye.cloneNode(true);
386
+ const leftEye = scoopTemplate.leftEye.cloneNode(true);
387
+ applyEyeColors(rightEye, leftEye, colorMode);
388
+ applyEyeTransforms(rightEye, leftEye, params);
389
+ coneGroup.appendChild(rightEye);
390
+ coneGroup.appendChild(leftEye);
391
+ }
392
+ mainGroup.appendChild(coneGroup);
393
+
394
+ // Add scoops from bottom to top
395
+ for (let i = 0; i < count; i++) {
396
+ const scoopGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
397
+ // Each scoop is pushed down by the weight of scoops above it
398
+ const scoopsAbove = (count - 1) - i;
399
+ const pushDown = scoopsAbove * conePush;
400
+ const scoopY = -i * offset + pushDown;
401
+ // Random wobble (bottom scoop stays centered)
402
+ const scoopX = i > 0 ? wobbleSeeds[i].x * wobble : 0;
403
+ const scoopAngle = i > 0 ? wobbleSeeds[i].angle * angularWobble : 0;
404
+ const scoopScale = Math.pow(scaleDecay, i);
405
+ const centerX = 288;
406
+ const centerY = 250;
407
+
408
+ scoopGroup.setAttribute('transform',
409
+ `translate(${centerX + scoopX}, ${centerY + scoopY}) rotate(${scoopAngle}) scale(${scoopScale}) translate(${-centerX}, ${-centerY})`);
410
+
411
+ const outline = scoopTemplate.outline.cloneNode(true);
412
+ applyScoopColors(outline, i, colorMode, svg);
413
+ scoopGroup.appendChild(outline);
414
+
415
+ // Only add eyes to top scoop
416
+ if (i === count - 1) {
417
+ const rightEye = scoopTemplate.rightEye.cloneNode(true);
418
+ const leftEye = scoopTemplate.leftEye.cloneNode(true);
419
+ applyEyeColors(rightEye, leftEye, colorMode);
420
+ applyEyeTransforms(rightEye, leftEye, params);
421
+ scoopGroup.appendChild(rightEye);
422
+ scoopGroup.appendChild(leftEye);
423
+ }
424
+
425
+ mainGroup.appendChild(scoopGroup);
426
+ }
427
+
428
+ svg.appendChild(mainGroup);
429
+ container.innerHTML = '';
430
+ container.appendChild(svg);
431
+ }
432
+
433
+ function applyConeColors(cone, colorMode) {
434
+ if (colorMode === 'mono-light') {
435
+ // White cone with black outlines
436
+ cone.querySelectorAll('[style*="fill"]').forEach(e => {
437
+ if (e.id === 'Cone_Bottom' || e.id === 'Cone_Top') {
438
+ e.style.fill = '#fff';
439
+ } else {
440
+ // Waffle cross-hatch
441
+ e.style.fill = '#000';
442
+ }
443
+ });
444
+ cone.querySelectorAll('path:not([style*="fill"]), rect:not([style*="fill"])').forEach(e => {
445
+ e.style.fill = '#000';
446
+ });
447
+ cone.querySelectorAll('[style*="stroke"]').forEach(e => {
448
+ e.style.stroke = '#000';
449
+ });
450
+ } else if (colorMode === 'mono-dark') {
451
+ // Dark gray cone with white outlines
452
+ cone.querySelectorAll('[style*="fill"]').forEach(e => {
453
+ if (e.id === 'Cone_Bottom' || e.id === 'Cone_Top') {
454
+ e.style.fill = '#222';
455
+ } else {
456
+ // Waffle cross-hatch
457
+ e.style.fill = '#fff';
458
+ }
459
+ });
460
+ cone.querySelectorAll('path:not([style*="fill"]), rect:not([style*="fill"])').forEach(e => {
461
+ e.style.fill = '#fff';
462
+ });
463
+ cone.querySelectorAll('[style*="stroke"]').forEach(e => {
464
+ e.style.stroke = '#fff';
465
+ });
466
+ } else {
467
+ // Color mode
468
+ cone.querySelectorAll('[style*="fill"]').forEach(e => {
469
+ if (e.id === 'Cone_Bottom' || e.id === 'Cone_Top') {
470
+ e.style.fill = coneColors.base;
471
+ } else {
472
+ // Waffle cross-hatch
473
+ e.style.fill = coneColors.crosshatch;
474
+ }
475
+ });
476
+ cone.querySelectorAll('path:not([style*="fill"]), rect:not([style*="fill"])').forEach(e => {
477
+ e.style.fill = coneColors.crosshatch;
478
+ });
479
+ cone.querySelectorAll('[style*="stroke"]').forEach(e => {
480
+ e.style.stroke = coneColors.outline;
481
+ });
482
+ }
483
+ }
484
+
485
+ function darkenColor(hex, percent = 20) {
486
+ // Convert hex to RGB
487
+ const r = parseInt(hex.slice(1, 3), 16);
488
+ const g = parseInt(hex.slice(3, 5), 16);
489
+ const b = parseInt(hex.slice(5, 7), 16);
490
+
491
+ // Darken by percentage
492
+ const newR = Math.max(0, Math.floor(r * (1 - percent / 100)));
493
+ const newG = Math.max(0, Math.floor(g * (1 - percent / 100)));
494
+ const newB = Math.max(0, Math.floor(b * (1 - percent / 100)));
495
+
496
+ // Convert back to hex
497
+ return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
498
+ }
499
+
500
+ function applyScoopColors(outline, scoopIndex, colorMode, svgRoot) {
501
+ if (colorMode === 'mono-light') {
502
+ outline.style.fill = '#fff';
503
+ outline.style.stroke = '#000';
504
+ } else if (colorMode === 'mono-dark') {
505
+ outline.style.fill = '#222';
506
+ outline.style.stroke = '#fff';
507
+ } else {
508
+ // Color mode - use custom color or default palette
509
+ const colorInput = document.getElementById(`scoop-color-${scoopIndex}`);
510
+ const baseColor = colorInput ? colorInput.value : scoopColors[scoopIndex % scoopColors.length].base;
511
+ const darkColor = scoopColors[scoopIndex % scoopColors.length].dark;
512
+
513
+ // Create outline color - darker than the dark gradient stop
514
+ const outlineColor = darkenColor(darkColor, 20);
515
+
516
+ // Create gradient in the SVG defs
517
+ const gradientId = `scoop-gradient-${scoopIndex}`;
518
+
519
+ let defs = svgRoot.querySelector('defs');
520
+ if (!defs) {
521
+ defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
522
+ svgRoot.insertBefore(defs, svgRoot.firstChild);
523
+ }
524
+
525
+ // Remove old gradient if it exists
526
+ const oldGradient = defs.querySelector(`#${gradientId}`);
527
+ if (oldGradient) {
528
+ oldGradient.remove();
529
+ }
530
+
531
+ const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
532
+ gradient.setAttribute('id', gradientId);
533
+ gradient.setAttribute('x1', '0%');
534
+ gradient.setAttribute('y1', '0%');
535
+ gradient.setAttribute('x2', '100%');
536
+ gradient.setAttribute('y2', '100%');
537
+
538
+ const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
539
+ stop1.setAttribute('offset', '0%');
540
+ stop1.setAttribute('style', `stop-color:${baseColor};stop-opacity:1`);
541
+
542
+ const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
543
+ stop2.setAttribute('offset', '100%');
544
+ stop2.setAttribute('style', `stop-color:${darkColor};stop-opacity:1`);
545
+
546
+ gradient.appendChild(stop1);
547
+ gradient.appendChild(stop2);
548
+ defs.appendChild(gradient);
549
+
550
+ outline.style.fill = `url(#${gradientId})`;
551
+ outline.style.stroke = outlineColor;
552
+ }
553
+ }
554
+
555
+ function applyEyeColors(rightEye, leftEye, colorMode) {
556
+ [rightEye, leftEye].forEach(eye => {
557
+ const outline = eye.querySelector('circle');
558
+ const pupil = eye.querySelector('path');
559
+
560
+ if (colorMode === 'mono-light') {
561
+ if (outline) {
562
+ outline.style.fill = '#fff';
563
+ outline.style.stroke = '#000';
564
+ }
565
+ if (pupil) pupil.style.fill = '#000';
566
+ } else if (colorMode === 'mono-dark') {
567
+ if (outline) {
568
+ outline.style.fill = '#000';
569
+ outline.style.stroke = '#fff';
570
+ }
571
+ if (pupil) pupil.style.fill = '#fff';
572
+ } else {
573
+ // Color mode
574
+ if (outline) {
575
+ outline.style.fill = '#fff';
576
+ outline.style.stroke = '#000';
577
+ }
578
+ if (pupil) pupil.style.fill = '#000';
579
+ }
580
+ });
581
+ }
582
+
583
+ function applyEyeTransforms(rightEye, leftEye, params) {
584
+ const { eyeAnimate, eyerollSpeed, eyeDistance, eyeSize, eyeAngle } = params;
585
+
586
+ // Get original eye positions from SVG
587
+ // Right eye center: 383.4, 216.12
588
+ // Left eye center: 175.1, 250.34
589
+ const rightEyeCenterX = 383.4;
590
+ const rightEyeCenterY = 216.12;
591
+ const leftEyeCenterX = 175.1;
592
+ const leftEyeCenterY = 250.34;
593
+ const eyeCenterX = (rightEyeCenterX + leftEyeCenterX) / 2;
594
+ const eyeCenterY = (rightEyeCenterY + leftEyeCenterY) / 2;
595
+
596
+ // Apply transforms to both eyes
597
+ [rightEye, leftEye].forEach((eye, idx) => {
598
+ const isRight = idx === 0;
599
+ const centerX = isRight ? rightEyeCenterX : leftEyeCenterX;
600
+ const centerY = isRight ? rightEyeCenterY : leftEyeCenterY;
601
+
602
+ // Build transform string for the whole eye group
603
+ let transforms = [];
604
+
605
+ // Eye distance: move eyes apart/together from center point
606
+ if (eyeDistance !== 1.0) {
607
+ const dx = (centerX - eyeCenterX) * (eyeDistance - 1);
608
+ const dy = (centerY - eyeCenterY) * (eyeDistance - 1);
609
+ transforms.push(`translate(${dx}, ${dy})`);
610
+ }
611
+
612
+ // Eye size: scale from eye center
613
+ if (eyeSize !== 1.0) {
614
+ transforms.push(`translate(${centerX}, ${centerY}) scale(${eyeSize}) translate(${-centerX}, ${-centerY})`);
615
+ }
616
+
617
+ // Eye angle: rotate from eye center
618
+ if (eyeAngle !== 0) {
619
+ transforms.push(`translate(${centerX}, ${centerY}) rotate(${eyeAngle}) translate(${-centerX}, ${-centerY})`);
620
+ }
621
+
622
+ if (transforms.length > 0) {
623
+ eye.setAttribute('transform', transforms.join(' '));
624
+ }
625
+
626
+ // Apply animation to the pupil only using the correct ID
627
+ const pupilId = isRight ? 'Right_Pupil' : 'Left_Pupil';
628
+ const pupil = eye.querySelector(`#${pupilId}`);
629
+ if (pupil && eyeAnimate) {
630
+ pupil.classList.add('pupil-animated');
631
+ pupil.style.animation = `eyeroll ${eyerollSpeed}s ease-in-out infinite`;
632
+ } else if (pupil) {
633
+ pupil.classList.remove('pupil-animated');
634
+ pupil.style.animation = '';
635
+ }
636
+ });
637
+ }
638
+
639
+ function render() {
640
+ const userCount = parseInt(document.getElementById('scoop-count').value);
641
+ const colorMode = document.getElementById('color-mode').value;
642
+
643
+ // Update body class based on color mode
644
+ document.body.classList.remove('light', 'dark');
645
+ if (colorMode === 'mono-dark') {
646
+ document.body.classList.add('dark');
647
+ } else {
648
+ document.body.classList.add('light');
649
+ }
650
+
651
+ const params = {
652
+ offset: parseInt(document.getElementById('scoop-offset').value),
653
+ scaleDecay: parseFloat(document.getElementById('scoop-scale').value),
654
+ wobble: parseInt(document.getElementById('scoop-wobble').value),
655
+ angularWobble: parseInt(document.getElementById('angular-wobble').value),
656
+ conePush: parseInt(document.getElementById('cone-push').value),
657
+ midrangeScale: parseFloat(document.getElementById('midrange-scale').value),
658
+ midrangeY: parseInt(document.getElementById('midrange-y').value),
659
+ eyeAnimate: document.getElementById('eye-animate').checked,
660
+ eyerollSpeed: parseFloat(document.getElementById('eyeroll-speed').value),
661
+ eyeDistance: parseFloat(document.getElementById('eye-distance').value),
662
+ eyeSize: parseFloat(document.getElementById('eye-size').value),
663
+ eyeAngle: parseFloat(document.getElementById('eye-angle').value),
664
+ colorMode: colorMode
665
+ };
666
+
667
+ // Update label
668
+ document.getElementById('label-user').textContent = `${userCount} Scoop${userCount !== 1 ? 's' : ''}`;
669
+
670
+ // Render all three variants
671
+ renderLogo(document.getElementById('logo-container-0'), 0, params);
672
+ renderLogo(document.getElementById('logo-container-user'), userCount, params);
673
+ renderLogo(document.getElementById('logo-container-10'), 10, params);
674
+ }
675
+
676
+
677
+
678
+ // Generate SVG string from container
679
+ function getSVGString(container) {
680
+ const svg = container.querySelector('svg');
681
+ if (!svg) return null;
682
+
683
+ const serializer = new XMLSerializer();
684
+ let svgString = serializer.serializeToString(svg);
685
+
686
+ // Add XML declaration
687
+ svgString = '<?xml version="1.0" encoding="UTF-8"?>\n' + svgString;
688
+
689
+ return svgString;
690
+ }
691
+
692
+ // Convert SVG to PNG (square)
693
+ function svgToPNG(svgString, size) {
694
+ return new Promise((resolve, reject) => {
695
+ const img = new Image();
696
+ const canvas = document.createElement('canvas');
697
+ canvas.width = size;
698
+ canvas.height = size;
699
+ const ctx = canvas.getContext('2d');
700
+
701
+ img.onload = () => {
702
+ ctx.drawImage(img, 0, 0, size, size);
703
+ canvas.toBlob(blob => {
704
+ resolve(blob);
705
+ }, 'image/png');
706
+ };
707
+
708
+ img.onerror = reject;
709
+
710
+ const blob = new Blob([svgString], { type: 'image/svg+xml' });
711
+ const url = URL.createObjectURL(blob);
712
+ img.src = url;
713
+ });
714
+ }
715
+
716
+ // Download a file
717
+ function downloadFile(blob, filename) {
718
+ const url = URL.createObjectURL(blob);
719
+ const a = document.createElement('a');
720
+ a.href = url;
721
+ a.download = filename;
722
+ document.body.appendChild(a);
723
+ a.click();
724
+ document.body.removeChild(a);
725
+ URL.revokeObjectURL(url);
726
+ }
727
+
728
+ // Generate all variants
729
+ async function generateAllVariants() {
730
+ const statusEl = document.getElementById('export-status');
731
+ statusEl.textContent = 'Generating variants...';
732
+
733
+ const modes = ['mono-light', 'mono-dark', 'color'];
734
+ const scoopCounts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
735
+ const pngSizes = [16, 32, 48, 128];
736
+
737
+ // Save current state
738
+ const originalMode = document.getElementById('color-mode').value;
739
+ const originalCount = parseInt(document.getElementById('scoop-count').value);
740
+
741
+ // Create temporary container for rendering
742
+ const tempContainer = document.createElement('div');
743
+ tempContainer.style.position = 'absolute';
744
+ tempContainer.style.left = '-9999px';
745
+ tempContainer.style.width = '300px';
746
+ tempContainer.style.height = '300px';
747
+ document.body.appendChild(tempContainer);
748
+
749
+ let totalFiles = 0;
750
+
751
+ try {
752
+ // Generate SVG variants (3 modes × 11 scoop counts = 33 files)
753
+ for (const mode of modes) {
754
+ for (const count of scoopCounts) {
755
+ statusEl.textContent = `Generating SVG: ${mode}, ${count} scoops...`;
756
+
757
+ // Set mode and count
758
+ document.getElementById('color-mode').value = mode;
759
+ document.getElementById('scoop-count').value = count;
760
+ generateColorControls();
761
+
762
+ // Get current params
763
+ const params = {
764
+ offset: parseInt(document.getElementById('scoop-offset').value),
765
+ scaleDecay: parseFloat(document.getElementById('scoop-scale').value),
766
+ wobble: parseInt(document.getElementById('scoop-wobble').value),
767
+ angularWobble: parseInt(document.getElementById('angular-wobble').value),
768
+ conePush: parseInt(document.getElementById('cone-push').value),
769
+ midrangeScale: parseFloat(document.getElementById('midrange-scale').value),
770
+ midrangeY: parseInt(document.getElementById('midrange-y').value),
771
+ eyeAnimate: false, // No animation in exports
772
+ eyerollSpeed: parseFloat(document.getElementById('eyeroll-speed').value),
773
+ eyeDistance: parseFloat(document.getElementById('eye-distance').value),
774
+ eyeSize: parseFloat(document.getElementById('eye-size').value),
775
+ eyeAngle: parseFloat(document.getElementById('eye-angle').value),
776
+ colorMode: mode
777
+ };
778
+
779
+ // Render to temp container
780
+ renderLogo(tempContainer, count, params);
781
+
782
+ // Get SVG string
783
+ const svgString = getSVGString(tempContainer);
784
+ if (svgString) {
785
+ const blob = new Blob([svgString], { type: 'image/svg+xml' });
786
+ const filename = `sliccy-${mode}-${count}scoops.svg`;
787
+ downloadFile(blob, filename);
788
+ totalFiles++;
789
+
790
+ // Small delay to avoid overwhelming the browser
791
+ await new Promise(resolve => setTimeout(resolve, 100));
792
+ }
793
+ }
794
+ }
795
+
796
+ // Generate PNG variants (only from color mode, 11 counts × 4 sizes = 44 files)
797
+ document.getElementById('color-mode').value = 'color';
798
+ generateColorControls();
799
+
800
+ for (const count of scoopCounts) {
801
+ for (const size of pngSizes) {
802
+ statusEl.textContent = `Generating PNG: ${count} scoops, ${size}x${size}...`;
803
+
804
+ const params = {
805
+ offset: parseInt(document.getElementById('scoop-offset').value),
806
+ scaleDecay: parseFloat(document.getElementById('scoop-scale').value),
807
+ wobble: parseInt(document.getElementById('scoop-wobble').value),
808
+ angularWobble: parseInt(document.getElementById('angular-wobble').value),
809
+ conePush: parseInt(document.getElementById('cone-push').value),
810
+ midrangeScale: parseFloat(document.getElementById('midrange-scale').value),
811
+ midrangeY: parseInt(document.getElementById('midrange-y').value),
812
+ eyeAnimate: false,
813
+ eyerollSpeed: parseFloat(document.getElementById('eyeroll-speed').value),
814
+ eyeDistance: parseFloat(document.getElementById('eye-distance').value),
815
+ eyeSize: parseFloat(document.getElementById('eye-size').value),
816
+ eyeAngle: parseFloat(document.getElementById('eye-angle').value),
817
+ colorMode: 'color'
818
+ };
819
+
820
+ renderLogo(tempContainer, count, params);
821
+
822
+ const svgString = getSVGString(tempContainer);
823
+ if (svgString) {
824
+ const pngBlob = await svgToPNG(svgString, size);
825
+ const filename = `sliccy-color-${count}scoops-${size}x${size}.png`;
826
+ downloadFile(pngBlob, filename);
827
+ totalFiles++;
828
+
829
+ await new Promise(resolve => setTimeout(resolve, 100));
830
+ }
831
+ }
832
+ }
833
+
834
+ statusEl.textContent = `✅ Generated ${totalFiles} files! (33 SVGs + 44 PNGs)`;
835
+
836
+ } catch (error) {
837
+ statusEl.textContent = `❌ Error: ${error.message}`;
838
+ console.error(error);
839
+ } finally {
840
+ // Restore original state
841
+ document.getElementById('color-mode').value = originalMode;
842
+ document.getElementById('scoop-count').value = originalCount;
843
+ generateColorControls();
844
+ render();
845
+
846
+ // Remove temp container
847
+ document.body.removeChild(tempContainer);
848
+ }
849
+ }
850
+
851
+ // Event listeners
852
+ document.getElementById('color-mode').addEventListener('change', () => {
853
+ generateColorControls();
854
+ render();
855
+ });
856
+
857
+ ['scoop-count', 'scoop-offset', 'scoop-scale', 'scoop-wobble', 'angular-wobble', 'cone-push', 'midrange-scale', 'midrange-y', 'eyeroll-speed', 'eye-distance', 'eye-size', 'eye-angle'].forEach(id => {
858
+ const el = document.getElementById(id);
859
+ el.addEventListener('input', () => {
860
+ document.getElementById(id + '-val').textContent = el.value;
861
+ if (id === 'scoop-count') generateColorControls();
862
+ render();
863
+ });
864
+ });
865
+
866
+ document.getElementById('eye-animate').addEventListener('change', render);
867
+
868
+ document.getElementById('randomize-wobble').addEventListener('click', () => {
869
+ wobbleSeeds = [];
870
+ render();
871
+ });
872
+
873
+ document.getElementById('generate-variants').addEventListener('click', generateAllVariants);
874
+
875
+ loadSVG();
876
+ </script>
877
+ </body>
878
+ </html>
879
+