fluxy-bot 0.10.17 → 0.10.19
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/cli/commands/update.ts +27 -7
- package/package.json +1 -1
- package/supervisor/widget.js +190 -225
- package/workspace/client/public/spritesheet.webp +0 -0
package/cli/commands/update.ts
CHANGED
|
@@ -88,13 +88,33 @@ export function registerUpdateCommand(program: Command) {
|
|
|
88
88
|
fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// Always update
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
// Always update framework files that ship with each version.
|
|
92
|
+
// These are not user-editable — user code lives in src/components/, src/pages/, backend/, etc.
|
|
93
|
+
const frameworkFiles = [
|
|
94
|
+
'workspace/client/index.html', // splash screen, SW registration, meta tags
|
|
95
|
+
'workspace/client/src/main.tsx', // React entry point, app-ready signal
|
|
96
|
+
];
|
|
97
|
+
for (const rel of frameworkFiles) {
|
|
98
|
+
const src = path.join(extracted, rel);
|
|
99
|
+
const dst = path.join(DATA_DIR, rel);
|
|
100
|
+
if (fs.existsSync(src)) {
|
|
101
|
+
fs.cpSync(src, dst, { force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Always update public assets that ship with the framework (animation spritesheet, icons).
|
|
106
|
+
// Only copy specific files — never overwrite user-added public assets.
|
|
107
|
+
const frameworkAssets = [
|
|
108
|
+
'spritesheet.webp',
|
|
109
|
+
];
|
|
110
|
+
const publicSrc = path.join(extracted, 'workspace', 'client', 'public');
|
|
111
|
+
const publicDst = path.join(DATA_DIR, 'workspace', 'client', 'public');
|
|
112
|
+
for (const asset of frameworkAssets) {
|
|
113
|
+
const src = path.join(publicSrc, asset);
|
|
114
|
+
const dst = path.join(publicDst, asset);
|
|
115
|
+
if (fs.existsSync(src)) {
|
|
116
|
+
fs.cpSync(src, dst, { force: true });
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
for (const file of ['package.json', 'vite.config.ts', 'vite.fluxy.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
|
package/package.json
CHANGED
package/supervisor/widget.js
CHANGED
|
@@ -34,28 +34,39 @@
|
|
|
34
34
|
panel.appendChild(iframe);
|
|
35
35
|
|
|
36
36
|
// ══════════════════════════════════════════════════════════════════
|
|
37
|
-
// ──
|
|
37
|
+
// ── Sprite Sheet Animation (Splash + Bubble) ─────────────────────
|
|
38
38
|
// ══════════════════════════════════════════════════════════════════
|
|
39
39
|
|
|
40
|
-
// ──
|
|
41
|
-
var
|
|
42
|
-
var
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
var
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
// ── Sprite config ──
|
|
41
|
+
var COLS = 16;
|
|
42
|
+
var FRAME_W = 125;
|
|
43
|
+
var FRAME_H = 120;
|
|
44
|
+
var DISPLAY_H = 58;
|
|
45
|
+
var DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
|
|
46
|
+
|
|
47
|
+
// Frame ranges (0-indexed)
|
|
48
|
+
var IDLE_START = 0, IDLE_END = 29;
|
|
49
|
+
var MELT_START = 30, MELT_END = 52;
|
|
50
|
+
var TRAVEL_START = 52, TRAVEL_END = 84;
|
|
51
|
+
var REFORM_START = 84, REFORM_END = 191;
|
|
52
|
+
|
|
53
|
+
// Timing
|
|
54
|
+
var FPS = 29;
|
|
55
|
+
var FRAME_MS = 1000 / FPS;
|
|
56
|
+
var IDLE_FPS = 22;
|
|
57
|
+
var IDLE_FRAME_MS = 1000 / IDLE_FPS;
|
|
58
|
+
var REFORM_FPS = 70;
|
|
59
|
+
var REFORM_FRAME_MS = 1000 / REFORM_FPS;
|
|
60
|
+
|
|
61
|
+
var TRAVEL_PX_PER_MS = 0.65;
|
|
62
|
+
var TRAVEL_MIN = 385;
|
|
63
|
+
var TRAVEL_MAX = 1150;
|
|
48
64
|
var MIN_SPLASH_MS = 2000;
|
|
49
65
|
var BUBBLE_SIZE = 60;
|
|
50
66
|
var BUBBLE_MARGIN = 24;
|
|
51
67
|
|
|
52
68
|
// ── Easing ──
|
|
53
|
-
function easeInCubic(t) { return t * t * t; }
|
|
54
69
|
function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
|
55
|
-
function easeOutElastic(t) {
|
|
56
|
-
if (t === 0 || t === 1) return t;
|
|
57
|
-
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1;
|
|
58
|
-
}
|
|
59
70
|
|
|
60
71
|
// ── Canvas setup (starts fullscreen for splash) ──
|
|
61
72
|
var canvas = document.createElement('canvas');
|
|
@@ -66,16 +77,17 @@
|
|
|
66
77
|
canvas.style.cssText = 'position:fixed;inset:0;width:100vw;height:100dvh;z-index:9999;pointer-events:none;';
|
|
67
78
|
document.body.appendChild(canvas);
|
|
68
79
|
|
|
69
|
-
var bubble = canvas;
|
|
80
|
+
var bubble = canvas;
|
|
70
81
|
var ctx = canvas.getContext('2d');
|
|
71
82
|
var dpr = window.devicePixelRatio || 1;
|
|
72
83
|
var W = 0, H = 0;
|
|
73
84
|
|
|
74
|
-
// ── Canvas phase: 'splash' | 'transitioning' | 'bubble' ──
|
|
85
|
+
// ── Canvas phase: 'splash' | 'transitioning' | 'bubble' | 'disabled' ──
|
|
75
86
|
var canvasPhase = 'splash';
|
|
76
87
|
var canvasCreatedAt = Date.now();
|
|
77
88
|
var appReady = false;
|
|
78
89
|
var hideAfterTransition = false;
|
|
90
|
+
var skipSplash = false;
|
|
79
91
|
|
|
80
92
|
function resizeCanvas() {
|
|
81
93
|
if (canvasPhase === 'bubble') {
|
|
@@ -94,105 +106,36 @@
|
|
|
94
106
|
resizeCanvas();
|
|
95
107
|
window.addEventListener('resize', function () {
|
|
96
108
|
resizeCanvas();
|
|
97
|
-
// Recalculate center position during splash
|
|
98
109
|
if (canvasPhase === 'splash' && animState === 'idle') {
|
|
99
110
|
center.x = Math.round(W / 2);
|
|
100
111
|
center.y = Math.round(H / 2);
|
|
101
112
|
}
|
|
102
113
|
});
|
|
103
114
|
|
|
104
|
-
// ──
|
|
105
|
-
var
|
|
106
|
-
var
|
|
107
|
-
var
|
|
108
|
-
var
|
|
109
|
-
var
|
|
110
|
-
var phaseStart = 0;
|
|
115
|
+
// ── Animation state ──
|
|
116
|
+
var spriteSheet = null;
|
|
117
|
+
var animState = 'loading';
|
|
118
|
+
var currentFrame = 0;
|
|
119
|
+
var idleDirection = 1;
|
|
120
|
+
var lastFrameTime = 0;
|
|
111
121
|
var travelDuration = 0;
|
|
122
|
+
var travelStartTime = 0;
|
|
123
|
+
var travelAngle = 0;
|
|
124
|
+
var travelDrawAngle = 0;
|
|
125
|
+
var travelFlip = 1;
|
|
112
126
|
|
|
113
127
|
var center = { x: 0, y: 0 };
|
|
114
128
|
var travelA = { x: 0, y: 0 };
|
|
115
129
|
var target = { x: 0, y: 0 };
|
|
116
130
|
|
|
117
|
-
// ──
|
|
118
|
-
function
|
|
119
|
-
this.localX = localX;
|
|
120
|
-
this.localY = localY;
|
|
121
|
-
this.r = r;
|
|
122
|
-
this.g = g;
|
|
123
|
-
this.b = b;
|
|
124
|
-
this.a = a;
|
|
125
|
-
this.x = 0;
|
|
126
|
-
this.y = 0;
|
|
127
|
-
|
|
128
|
-
var normalizedY = (localY + displayH / 2) / displayH;
|
|
129
|
-
|
|
130
|
-
// Melt into an elliptical blob
|
|
131
|
-
var angle = Math.random() * Math.PI * 2;
|
|
132
|
-
var dist = Math.sqrt(Math.random());
|
|
133
|
-
var blobRX = displayW * 0.28;
|
|
134
|
-
var blobRY = displayH * 0.18;
|
|
135
|
-
this.meltLocalX = Math.cos(angle) * dist * blobRX;
|
|
136
|
-
this.meltLocalY = Math.sin(angle) * dist * blobRY + displayH * 0.3;
|
|
137
|
-
|
|
138
|
-
// Stagger: top particles melt first
|
|
139
|
-
this.meltDelay = normalizedY * 0.35;
|
|
140
|
-
|
|
141
|
-
this.wobbleAmp = 0.5 + Math.random() * 1.5;
|
|
142
|
-
this.wobbleFreq = 3 + Math.random() * 4;
|
|
143
|
-
this.wobblePhase = Math.random() * Math.PI * 2;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ── Load mascot ──
|
|
147
|
-
function loadMascot(onDone, onFail) {
|
|
131
|
+
// ── Load sprite sheet ──
|
|
132
|
+
function loadSprite(onDone, onFail) {
|
|
148
133
|
var img = new Image();
|
|
149
134
|
img.onload = function () {
|
|
150
|
-
|
|
151
|
-
displayH = TARGET_HEIGHT;
|
|
152
|
-
displayW = TARGET_HEIGHT * aspect;
|
|
153
|
-
|
|
154
|
-
// Pre-render at high DPR for crisp display
|
|
155
|
-
var preScale = dpr * 2;
|
|
156
|
-
var pre = document.createElement('canvas');
|
|
157
|
-
var preCtx = pre.getContext('2d');
|
|
158
|
-
pre.width = Math.round(displayW * preScale);
|
|
159
|
-
pre.height = Math.round(displayH * preScale);
|
|
160
|
-
preCtx.imageSmoothingEnabled = true;
|
|
161
|
-
preCtx.imageSmoothingQuality = 'high';
|
|
162
|
-
preCtx.drawImage(img, 0, 0, pre.width, pre.height);
|
|
163
|
-
preRendered = pre;
|
|
164
|
-
|
|
165
|
-
// Sample pixels for particles
|
|
166
|
-
var sampleH = SAMPLE_RES;
|
|
167
|
-
var sampleW = Math.round(SAMPLE_RES * aspect);
|
|
168
|
-
|
|
169
|
-
var off = document.createElement('canvas');
|
|
170
|
-
var offCtx = off.getContext('2d');
|
|
171
|
-
off.width = sampleW;
|
|
172
|
-
off.height = sampleH;
|
|
173
|
-
offCtx.imageSmoothingEnabled = true;
|
|
174
|
-
offCtx.imageSmoothingQuality = 'high';
|
|
175
|
-
offCtx.drawImage(img, 0, 0, sampleW, sampleH);
|
|
176
|
-
|
|
177
|
-
var data = offCtx.getImageData(0, 0, sampleW, sampleH).data;
|
|
178
|
-
var scaleX = displayW / sampleW;
|
|
179
|
-
var scaleY = displayH / sampleH;
|
|
180
|
-
particleRadius = (scaleX + scaleY) / 2 * 0.7;
|
|
181
|
-
|
|
182
|
-
for (var y = 0; y < sampleH; y++) {
|
|
183
|
-
for (var x = 0; x < sampleW; x++) {
|
|
184
|
-
var i = (y * sampleW + x) * 4;
|
|
185
|
-
if (data[i + 3] > 50) {
|
|
186
|
-
var lx = (x - sampleW / 2) * scaleX;
|
|
187
|
-
var ly = (y - sampleH / 2) * scaleY;
|
|
188
|
-
particles.push(new Particle(lx, ly, data[i], data[i+1], data[i+2], data[i+3] / 255));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Start idle in center
|
|
135
|
+
spriteSheet = img;
|
|
194
136
|
center.x = Math.round(W / 2);
|
|
195
137
|
center.y = Math.round(H / 2);
|
|
138
|
+
currentFrame = IDLE_START;
|
|
196
139
|
animState = 'idle';
|
|
197
140
|
|
|
198
141
|
// Hide the HTML splash fallback
|
|
@@ -204,7 +147,7 @@
|
|
|
204
147
|
img.onerror = function () {
|
|
205
148
|
if (onFail) onFail();
|
|
206
149
|
};
|
|
207
|
-
img.src = '/
|
|
150
|
+
img.src = '/spritesheet.webp';
|
|
208
151
|
}
|
|
209
152
|
|
|
210
153
|
// ── Trigger move ──
|
|
@@ -214,122 +157,129 @@
|
|
|
214
157
|
target.y = ty;
|
|
215
158
|
travelA.x = center.x;
|
|
216
159
|
travelA.y = center.y;
|
|
160
|
+
|
|
161
|
+
var dx = target.x - center.x;
|
|
162
|
+
var dy = target.y - center.y;
|
|
163
|
+
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
164
|
+
|
|
165
|
+
travelAngle = Math.atan2(dy, dx);
|
|
166
|
+
travelDrawAngle = travelAngle;
|
|
167
|
+
travelFlip = 1;
|
|
168
|
+
if (travelDrawAngle > Math.PI / 2) { travelDrawAngle -= Math.PI; travelFlip = -1; }
|
|
169
|
+
else if (travelDrawAngle < -Math.PI / 2) { travelDrawAngle += Math.PI; travelFlip = -1; }
|
|
170
|
+
travelDuration = Math.min(TRAVEL_MAX, Math.max(TRAVEL_MIN, dist / TRAVEL_PX_PER_MS));
|
|
171
|
+
|
|
172
|
+
currentFrame = MELT_START;
|
|
173
|
+
lastFrameTime = performance.now();
|
|
217
174
|
animState = 'melting';
|
|
218
|
-
phaseStart = performance.now();
|
|
219
175
|
}
|
|
220
176
|
|
|
221
177
|
// ── Update ──
|
|
222
178
|
function update(now) {
|
|
223
|
-
|
|
224
|
-
|
|
179
|
+
// Smooth position update during travel (every rAF)
|
|
180
|
+
if (animState === 'traveling') {
|
|
181
|
+
var elapsed = now - travelStartTime;
|
|
182
|
+
var progress = Math.min(1, elapsed / travelDuration);
|
|
183
|
+
var e = easeInOutCubic(progress);
|
|
184
|
+
center.x = travelA.x + (target.x - travelA.x) * e;
|
|
185
|
+
center.y = travelA.y + (target.y - travelA.y) * e;
|
|
225
186
|
|
|
226
|
-
if (animState === 'melting') {
|
|
227
|
-
var progress = elapsed / MELT_DURATION;
|
|
228
187
|
if (progress >= 1) {
|
|
229
|
-
progress = 1;
|
|
230
|
-
var dx = target.x - travelA.x;
|
|
231
|
-
var dy = target.y - travelA.y;
|
|
232
|
-
var dist = Math.sqrt(dx*dx + dy*dy);
|
|
233
|
-
travelDuration = Math.min(TRAVEL_MAX, Math.max(TRAVEL_MIN, dist / TRAVEL_PX_PER_MS));
|
|
234
|
-
animState = 'traveling';
|
|
235
|
-
phaseStart = now;
|
|
236
|
-
}
|
|
237
|
-
for (i = 0; i < particles.length; i++) {
|
|
238
|
-
p = particles[i];
|
|
239
|
-
t = Math.max(0, Math.min(1, (progress - p.meltDelay) / (1 - p.meltDelay)));
|
|
240
|
-
e = easeInCubic(t);
|
|
241
|
-
p.x = center.x + p.localX + (p.meltLocalX - p.localX) * e;
|
|
242
|
-
p.y = center.y + p.localY + (p.meltLocalY - p.localY) * e;
|
|
243
|
-
}
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (animState === 'traveling') {
|
|
248
|
-
var progress2 = elapsed / travelDuration;
|
|
249
|
-
if (progress2 >= 1) {
|
|
250
|
-
progress2 = 1;
|
|
251
188
|
center.x = target.x;
|
|
252
189
|
center.y = target.y;
|
|
253
190
|
animState = 'reforming';
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
cx = travelA.x + (target.x - travelA.x) * e;
|
|
258
|
-
cy = travelA.y + (target.y - travelA.y) * e;
|
|
259
|
-
for (i = 0; i < particles.length; i++) {
|
|
260
|
-
p = particles[i];
|
|
261
|
-
wx = Math.sin(now * 0.008 * p.wobbleFreq + p.wobblePhase) * p.wobbleAmp;
|
|
262
|
-
wy = Math.cos(now * 0.006 * p.wobbleFreq + p.wobblePhase) * p.wobbleAmp * 0.5;
|
|
263
|
-
p.x = cx + p.meltLocalX + wx;
|
|
264
|
-
p.y = cy + p.meltLocalY + wy;
|
|
191
|
+
currentFrame = REFORM_START;
|
|
192
|
+
lastFrameTime = now;
|
|
193
|
+
return;
|
|
265
194
|
}
|
|
266
|
-
return;
|
|
267
195
|
}
|
|
268
196
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
197
|
+
// Sprite frame advance (at configured FPS)
|
|
198
|
+
var frameInterval = animState === 'idle' ? IDLE_FRAME_MS
|
|
199
|
+
: animState === 'reforming' ? REFORM_FRAME_MS
|
|
200
|
+
: FRAME_MS;
|
|
201
|
+
var frameDelta = now - lastFrameTime;
|
|
202
|
+
if (frameDelta < frameInterval) return;
|
|
203
|
+
lastFrameTime = now - (frameDelta % frameInterval);
|
|
204
|
+
|
|
205
|
+
if (animState === 'idle') {
|
|
206
|
+
currentFrame += idleDirection;
|
|
207
|
+
if (currentFrame >= IDLE_END) { currentFrame = IDLE_END; idleDirection = -1; }
|
|
208
|
+
else if (currentFrame <= IDLE_START) { currentFrame = IDLE_START; idleDirection = 1; }
|
|
209
|
+
} else if (animState === 'melting') {
|
|
210
|
+
currentFrame++;
|
|
211
|
+
if (currentFrame > MELT_END) {
|
|
212
|
+
animState = 'traveling';
|
|
213
|
+
currentFrame = TRAVEL_START;
|
|
214
|
+
travelStartTime = now;
|
|
215
|
+
}
|
|
216
|
+
} else if (animState === 'traveling') {
|
|
217
|
+
currentFrame++;
|
|
218
|
+
if (currentFrame > TRAVEL_END) currentFrame = TRAVEL_START;
|
|
219
|
+
} else if (animState === 'reforming') {
|
|
220
|
+
currentFrame++;
|
|
221
|
+
if (currentFrame > REFORM_END) {
|
|
273
222
|
animState = 'idle';
|
|
223
|
+
currentFrame = IDLE_START;
|
|
224
|
+
idleDirection = 1;
|
|
274
225
|
// Transition canvas to bubble mode
|
|
275
226
|
if (canvasPhase === 'transitioning') {
|
|
276
227
|
canvasPhase = 'bubble';
|
|
277
|
-
canvas.style.cssText = 'position:fixed;bottom:' + BUBBLE_MARGIN + 'px;right:' + BUBBLE_MARGIN + 'px;width:' + BUBBLE_SIZE + 'px;height:' + BUBBLE_SIZE + 'px;z-index:99998;cursor:pointer;border-radius:50%;';
|
|
228
|
+
canvas.style.cssText = 'position:fixed;bottom:' + BUBBLE_MARGIN + 'px;right:' + BUBBLE_MARGIN + 'px;width:' + BUBBLE_SIZE + 'px;height:' + BUBBLE_SIZE + 'px;z-index:99998;cursor:pointer;border-radius:50%;transition:transform .15s ease;-webkit-tap-highlight-color:transparent;';
|
|
278
229
|
W = BUBBLE_SIZE;
|
|
279
230
|
H = BUBBLE_SIZE;
|
|
280
231
|
canvas.width = BUBBLE_SIZE * dpr;
|
|
281
232
|
canvas.height = BUBBLE_SIZE * dpr;
|
|
282
233
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
283
|
-
// Center is now the center of the bubble canvas
|
|
284
234
|
center.x = BUBBLE_SIZE / 2;
|
|
285
235
|
center.y = BUBBLE_SIZE / 2;
|
|
286
|
-
// Handle onboard hide
|
|
287
236
|
if (hideAfterTransition) {
|
|
288
237
|
canvas.style.display = 'none';
|
|
289
238
|
}
|
|
290
239
|
}
|
|
291
240
|
}
|
|
292
|
-
e = easeOutElastic(progress3);
|
|
293
|
-
for (i = 0; i < particles.length; i++) {
|
|
294
|
-
p = particles[i];
|
|
295
|
-
p.x = center.x + p.meltLocalX + (p.localX - p.meltLocalX) * e;
|
|
296
|
-
p.y = center.y + p.meltLocalY + (p.localY - p.meltLocalY) * e;
|
|
297
|
-
}
|
|
298
241
|
}
|
|
299
242
|
}
|
|
300
243
|
|
|
301
244
|
// ── Draw ──
|
|
245
|
+
function drawFrame(frame, x, y, rotate, flipX) {
|
|
246
|
+
if (!spriteSheet) return;
|
|
247
|
+
var col = frame % COLS;
|
|
248
|
+
var row = Math.floor(frame / COLS);
|
|
249
|
+
var sx = col * FRAME_W;
|
|
250
|
+
var sy = row * FRAME_H;
|
|
251
|
+
|
|
252
|
+
ctx.save();
|
|
253
|
+
ctx.translate(x, y);
|
|
254
|
+
if (rotate) ctx.rotate(rotate);
|
|
255
|
+
if (flipX === -1) ctx.scale(-1, 1);
|
|
256
|
+
ctx.imageSmoothingEnabled = true;
|
|
257
|
+
ctx.imageSmoothingQuality = 'high';
|
|
258
|
+
ctx.drawImage(
|
|
259
|
+
spriteSheet,
|
|
260
|
+
sx, sy, FRAME_W, FRAME_H,
|
|
261
|
+
-DISPLAY_W / 2, -DISPLAY_H / 2,
|
|
262
|
+
DISPLAY_W, DISPLAY_H
|
|
263
|
+
);
|
|
264
|
+
ctx.restore();
|
|
265
|
+
}
|
|
266
|
+
|
|
302
267
|
function draw(now) {
|
|
303
268
|
ctx.clearRect(0, 0, W, H);
|
|
269
|
+
if (!spriteSheet || animState === 'loading') return;
|
|
304
270
|
|
|
305
|
-
if (animState === '
|
|
306
|
-
var
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
var r = particleRadius;
|
|
318
|
-
for (var i = 0; i < particles.length; i++) {
|
|
319
|
-
var p = particles[i];
|
|
320
|
-
// In transitioning/bubble phase, offset particles relative to canvas
|
|
321
|
-
var px = p.x;
|
|
322
|
-
var py = p.y;
|
|
323
|
-
if (canvasPhase === 'splash' || canvasPhase === 'transitioning') {
|
|
324
|
-
// Particles are in screen coordinates during splash/transition
|
|
325
|
-
}
|
|
326
|
-
ctx.globalAlpha = p.a;
|
|
327
|
-
ctx.fillStyle = 'rgb(' + p.r + ',' + p.g + ',' + p.b + ')';
|
|
328
|
-
ctx.beginPath();
|
|
329
|
-
ctx.arc(px, py, r, 0, Math.PI * 2);
|
|
330
|
-
ctx.fill();
|
|
331
|
-
}
|
|
332
|
-
ctx.globalAlpha = 1;
|
|
271
|
+
if (animState === 'melting') {
|
|
272
|
+
var p = (currentFrame - MELT_START) / (MELT_END - MELT_START);
|
|
273
|
+
var eased = p * p;
|
|
274
|
+
drawFrame(currentFrame, center.x, center.y, travelDrawAngle * eased, travelFlip);
|
|
275
|
+
} else if (animState === 'traveling') {
|
|
276
|
+
drawFrame(currentFrame, center.x, center.y, travelDrawAngle, travelFlip);
|
|
277
|
+
} else if (animState === 'reforming') {
|
|
278
|
+
var p2 = (currentFrame - REFORM_START) / (REFORM_END - REFORM_START);
|
|
279
|
+
var eased2 = 1 - (1 - p2) * (1 - p2);
|
|
280
|
+
drawFrame(currentFrame, center.x, center.y, travelDrawAngle * (1 - eased2));
|
|
281
|
+
} else {
|
|
282
|
+
drawFrame(currentFrame, center.x, center.y);
|
|
333
283
|
}
|
|
334
284
|
}
|
|
335
285
|
|
|
@@ -355,34 +305,70 @@
|
|
|
355
305
|
|
|
356
306
|
function triggerTransition() {
|
|
357
307
|
canvasPhase = 'transitioning';
|
|
358
|
-
// Target: center of the 60x60 bubble area at bottom-right
|
|
359
308
|
var tx = W - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
|
|
360
309
|
var ty = H - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
|
|
361
310
|
moveTo(tx, ty);
|
|
362
311
|
}
|
|
363
312
|
|
|
313
|
+
// Skip splash and go straight to bubble mode (for onboard / instant load)
|
|
314
|
+
function skipToBubble() {
|
|
315
|
+
canvasPhase = 'bubble';
|
|
316
|
+
canvas.style.cssText = 'position:fixed;bottom:' + BUBBLE_MARGIN + 'px;right:' + BUBBLE_MARGIN + 'px;width:' + BUBBLE_SIZE + 'px;height:' + BUBBLE_SIZE + 'px;z-index:99998;cursor:pointer;border-radius:50%;transition:transform .15s ease;-webkit-tap-highlight-color:transparent;';
|
|
317
|
+
W = BUBBLE_SIZE;
|
|
318
|
+
H = BUBBLE_SIZE;
|
|
319
|
+
canvas.width = BUBBLE_SIZE * dpr;
|
|
320
|
+
canvas.height = BUBBLE_SIZE * dpr;
|
|
321
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
322
|
+
center.x = BUBBLE_SIZE / 2;
|
|
323
|
+
center.y = BUBBLE_SIZE / 2;
|
|
324
|
+
currentFrame = IDLE_START;
|
|
325
|
+
idleDirection = 1;
|
|
326
|
+
animState = 'idle';
|
|
327
|
+
// Hide the HTML splash fallback
|
|
328
|
+
var splash = document.getElementById('splash');
|
|
329
|
+
if (splash) splash.style.display = 'none';
|
|
330
|
+
}
|
|
331
|
+
|
|
364
332
|
window.addEventListener('fluxy:app-ready', function () {
|
|
365
333
|
appReady = true;
|
|
366
334
|
maybeTransition();
|
|
367
335
|
});
|
|
368
|
-
// Race condition guard: check if main.tsx already fired before we registered
|
|
369
336
|
if (window.__fluxyAppReady) {
|
|
370
337
|
appReady = true;
|
|
371
338
|
}
|
|
372
339
|
|
|
373
|
-
// ──
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
340
|
+
// ── Check onboard status, then start ──
|
|
341
|
+
// First-time users get the onboard wizard — skip the splash animation.
|
|
342
|
+
var onboardActive = false;
|
|
343
|
+
fetch('/api/settings')
|
|
344
|
+
.then(function (r) { return r.json(); })
|
|
345
|
+
.then(function (s) {
|
|
346
|
+
if (s.onboard_complete !== 'true') {
|
|
347
|
+
onboardActive = true;
|
|
348
|
+
skipSplash = true;
|
|
349
|
+
}
|
|
350
|
+
startAnimation();
|
|
351
|
+
})
|
|
352
|
+
.catch(function () {
|
|
353
|
+
startAnimation();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
function startAnimation() {
|
|
357
|
+
loadSprite(
|
|
358
|
+
function onSuccess() {
|
|
359
|
+
if (skipSplash) {
|
|
360
|
+
skipToBubble();
|
|
361
|
+
if (onboardActive) canvas.style.display = 'none';
|
|
362
|
+
}
|
|
363
|
+
requestAnimationFrame(loop);
|
|
364
|
+
maybeTransition();
|
|
365
|
+
},
|
|
366
|
+
function onFail() {
|
|
367
|
+
canvas.style.display = 'none';
|
|
368
|
+
canvasPhase = 'disabled';
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
}
|
|
386
372
|
|
|
387
373
|
// ══════════════════════════════════════════════════════════════════
|
|
388
374
|
// ── Widget Interaction (Panel, Toggle, Messages) ─────────────────
|
|
@@ -391,7 +377,7 @@
|
|
|
391
377
|
var isOpen = false;
|
|
392
378
|
|
|
393
379
|
function toggle() {
|
|
394
|
-
if (canvasPhase !== 'bubble') return;
|
|
380
|
+
if (canvasPhase !== 'bubble') return;
|
|
395
381
|
isOpen = !isOpen;
|
|
396
382
|
panel.classList.toggle('open', isOpen);
|
|
397
383
|
backdrop.classList.toggle('open', isOpen);
|
|
@@ -419,10 +405,8 @@
|
|
|
419
405
|
window.addEventListener('message', function (e) {
|
|
420
406
|
if (!e.data || !e.data.type) return;
|
|
421
407
|
|
|
422
|
-
// Close chat panel
|
|
423
408
|
if (e.data.type === 'fluxy:close' && isOpen) toggle();
|
|
424
409
|
|
|
425
|
-
// Install App request from chat iframe
|
|
426
410
|
if (e.data.type === 'fluxy:install-app') {
|
|
427
411
|
if (deferredInstallPrompt) {
|
|
428
412
|
deferredInstallPrompt.prompt();
|
|
@@ -430,46 +414,27 @@
|
|
|
430
414
|
if (result.outcome === 'accepted') deferredInstallPrompt = null;
|
|
431
415
|
});
|
|
432
416
|
} else {
|
|
433
|
-
// No native prompt available — tell chat to show instructions modal
|
|
434
417
|
iframe.contentWindow.postMessage({ type: 'fluxy:show-ios-install' }, '*');
|
|
435
418
|
}
|
|
436
419
|
}
|
|
420
|
+
|
|
421
|
+
// Onboard complete — show the bubble
|
|
422
|
+
if (e.data.type === 'fluxy:onboard-complete') {
|
|
423
|
+
onboardActive = false;
|
|
424
|
+
hideAfterTransition = false;
|
|
425
|
+
canvas.style.display = 'block';
|
|
426
|
+
}
|
|
437
427
|
});
|
|
438
428
|
|
|
439
|
-
// Restore open state after HMR reload
|
|
429
|
+
// Restore open state after HMR reload
|
|
440
430
|
try {
|
|
441
431
|
if (sessionStorage.getItem('fluxy_widget_open') === '1') {
|
|
442
432
|
sessionStorage.removeItem('fluxy_widget_open');
|
|
443
|
-
// Only restore if already in bubble phase
|
|
444
433
|
if (canvasPhase === 'bubble') toggle();
|
|
445
434
|
}
|
|
446
435
|
} catch (e) {}
|
|
447
436
|
|
|
448
|
-
// Hide widget during initial onboard
|
|
449
|
-
try {
|
|
450
|
-
fetch('/api/settings')
|
|
451
|
-
.then(function (r) { return r.json(); })
|
|
452
|
-
.then(function (s) {
|
|
453
|
-
if (s.onboard_complete !== 'true') {
|
|
454
|
-
if (canvasPhase === 'bubble') {
|
|
455
|
-
canvas.style.display = 'none';
|
|
456
|
-
} else {
|
|
457
|
-
hideAfterTransition = true;
|
|
458
|
-
}
|
|
459
|
-
window.addEventListener('message', function onMsg(e) {
|
|
460
|
-
if (e.data && e.data.type === 'fluxy:onboard-complete') {
|
|
461
|
-
hideAfterTransition = false;
|
|
462
|
-
canvas.style.display = 'block';
|
|
463
|
-
window.removeEventListener('message', onMsg);
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
})
|
|
468
|
-
.catch(function () {});
|
|
469
|
-
} catch (e) {}
|
|
470
|
-
|
|
471
437
|
// Inject app-ws.js — proxies /app/api/* fetch calls through WebSocket
|
|
472
|
-
// (works around POST request failures through Cloudflare tunnel)
|
|
473
438
|
var awsScript = document.createElement('script');
|
|
474
439
|
awsScript.src = '/fluxy/app-ws.js';
|
|
475
440
|
document.head.appendChild(awsScript);
|
|
Binary file
|