partycles 1.0.0 → 1.1.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/README.md CHANGED
@@ -359,7 +359,7 @@ function Achievement({ unlocked, name }) {
359
359
  1. **Unique IDs**: Ensure each animated element has a unique ID
360
360
  2. **Performance**: Avoid triggering multiple animations simultaneously
361
361
  3. **Accessibility**: Provide alternative feedback for users who prefer reduced motion
362
- 4. **Mobile**: Test animations on mobile devices and adjust particle counts if needed
362
+ 4. **Mobile**: Partycles automatically optimizes for mobile devices
363
363
 
364
364
  ```tsx
365
365
  // Respect user preferences
@@ -370,6 +370,32 @@ const { reward } = useReward('buttonId', 'confetti', {
370
370
  });
371
371
  ```
372
372
 
373
+ ## 📱 Mobile Optimization
374
+
375
+ Partycles automatically detects mobile devices and optimizes performance:
376
+
377
+ - **Reduced particle counts** (60% of desktop)
378
+ - **Smaller particle sizes** (80% of desktop)
379
+ - **Shorter lifetimes** (80% of desktop)
380
+ - **Frame skipping** for smoother performance
381
+ - **Tab visibility detection** to pause when inactive
382
+
383
+ You can also manually check for mobile devices:
384
+
385
+ ```tsx
386
+ import { isMobileDevice, optimizeConfigForMobile } from 'partycles';
387
+
388
+ if (isMobileDevice()) {
389
+ // Custom mobile logic
390
+ }
391
+
392
+ // Or manually optimize a config
393
+ const mobileConfig = optimizeConfigForMobile({
394
+ particleCount: 100,
395
+ elementSize: 30
396
+ });
397
+ ```
398
+
373
399
  ## 🔧 Advanced Usage
374
400
 
375
401
  ### Custom Physics
@@ -1,4 +1,5 @@
1
1
  export { useReward } from './useReward';
2
2
  export type { AnimationType, AnimationConfig, UseRewardConfig } from './types';
3
3
  export { emojiPresets } from './animations/emoji';
4
+ export { isMobileDevice, optimizeConfigForMobile } from './mobileOptimizations';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { AnimationConfig } from './types';
2
+ export declare const isMobileDevice: () => boolean;
3
+ export declare const optimizeConfigForMobile: (config: AnimationConfig) => AnimationConfig;
4
+ export declare const shouldSkipFrame: (frameCount: number) => boolean;
5
+ export declare const simplifyVisualEffects: (animationType: string) => boolean;
6
+ //# sourceMappingURL=mobileOptimizations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobileOptimizations.d.ts","sourceRoot":"","sources":["../src/mobileOptimizations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,eAAO,MAAM,cAAc,QAAO,OAcjC,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,eAAe,KAAG,eAoBjE,CAAC;AAGF,eAAO,MAAM,eAAe,GAAI,YAAY,MAAM,KAAG,OAIpD,CAAC;AAGF,eAAO,MAAM,qBAAqB,GAAI,eAAe,MAAM,KAAG,OAK7D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useReward.d.ts","sourceRoot":"","sources":["../src/useReward.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAY,MAAM,SAAS,CAAC;AAInE,UAAU,eAAe;IACvB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,SAAS,GACpB,WAAW,MAAM,EACjB,eAAe,aAAa,EAC5B,SAAS,eAAe,KACvB,eAkJF,CAAC"}
1
+ {"version":3,"file":"useReward.d.ts","sourceRoot":"","sources":["../src/useReward.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAY,MAAM,SAAS,CAAC;AAKnE,UAAU,eAAe;IACvB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,SAAS,GACpB,WAAW,MAAM,EACjB,eAAe,aAAa,EAC5B,SAAS,eAAe,KACvB,eAuKF,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { useReward } from './useReward';
2
2
  export type { AnimationType, AnimationConfig, UseRewardConfig } from './types';
3
3
  export { emojiPresets } from './animations/emoji';
4
+ export { isMobileDevice, optimizeConfigForMobile } from './mobileOptimizations';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useCallback, useEffect } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
 
4
4
  const randomInRange = (min, max) => {
@@ -1150,12 +1150,57 @@ const animations = {
1150
1150
  },
1151
1151
  };
1152
1152
 
1153
+ const isMobileDevice = () => {
1154
+ if (typeof window === 'undefined')
1155
+ return false;
1156
+ // Check user agent
1157
+ const userAgent = navigator.userAgent || navigator.vendor || window.opera;
1158
+ const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
1159
+ // Check viewport width
1160
+ const isMobileWidth = window.innerWidth <= 768;
1161
+ // Check touch support
1162
+ const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
1163
+ return isMobileUA || (isMobileWidth && hasTouch);
1164
+ };
1165
+ const optimizeConfigForMobile = (config) => {
1166
+ var _a, _b, _c;
1167
+ if (!isMobileDevice())
1168
+ return config;
1169
+ return Object.assign(Object.assign({}, config), {
1170
+ // Reduce particle count by 40%
1171
+ particleCount: Math.floor((config.particleCount || 50) * 0.6),
1172
+ // Reduce element size by 20%
1173
+ elementSize: Math.floor((config.elementSize || 20) * 0.8),
1174
+ // Reduce lifetime by 20%
1175
+ lifetime: Math.floor((config.lifetime || 150) * 0.8),
1176
+ // Simplify physics
1177
+ physics: Object.assign(Object.assign({}, config.physics), {
1178
+ // Reduce precision for mobile
1179
+ gravity: Math.round((((_a = config.physics) === null || _a === void 0 ? void 0 : _a.gravity) || 0) * 100) / 100, wind: Math.round((((_b = config.physics) === null || _b === void 0 ? void 0 : _b.wind) || 0) * 100) / 100, friction: Math.round((((_c = config.physics) === null || _c === void 0 ? void 0 : _c.friction) || 0.98) * 100) / 100 }) });
1180
+ };
1181
+ // Frame skipping for mobile
1182
+ const shouldSkipFrame = (frameCount) => {
1183
+ if (!isMobileDevice())
1184
+ return false;
1185
+ // Skip every 3rd frame on mobile
1186
+ return frameCount % 3 === 0;
1187
+ };
1188
+
1153
1189
  const useReward = (elementId, animationType, config) => {
1154
1190
  const [isAnimating, setIsAnimating] = useState(false);
1155
1191
  const animationFrameRef = useRef();
1156
1192
  const particlesRef = useRef([]);
1157
1193
  const containerRef = useRef(null);
1158
1194
  const rootRef = useRef(null);
1195
+ const isTabVisible = useRef(true);
1196
+ // Monitor tab visibility
1197
+ useEffect(() => {
1198
+ const handleVisibilityChange = () => {
1199
+ isTabVisible.current = !document.hidden;
1200
+ };
1201
+ document.addEventListener('visibilitychange', handleVisibilityChange);
1202
+ return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
1203
+ }, []);
1159
1204
  const animate = useCallback(() => {
1160
1205
  var _a, _b, _c, _d, _e, _f;
1161
1206
  const element = document.getElementById(elementId);
@@ -1171,8 +1216,10 @@ const useReward = (elementId, animationType, config) => {
1171
1216
  console.error(`Animation type "${animationType}" not found`);
1172
1217
  return;
1173
1218
  }
1219
+ // Apply mobile performance optimizations
1220
+ const optimizedConfig = config ? optimizeConfigForMobile(config) : undefined;
1174
1221
  // Create particles
1175
- particlesRef.current = animationHandler.createParticles(origin, config || {});
1222
+ particlesRef.current = animationHandler.createParticles(origin, optimizedConfig || {});
1176
1223
  // Create container
1177
1224
  const container = document.createElement('div');
1178
1225
  container.style.position = 'fixed';
@@ -1193,8 +1240,13 @@ const useReward = (elementId, animationType, config) => {
1193
1240
  const gravity = (_b = (_a = config === null || config === void 0 ? void 0 : config.physics) === null || _a === void 0 ? void 0 : _a.gravity) !== null && _b !== void 0 ? _b : defaultGravity;
1194
1241
  const friction = (_d = (_c = config === null || config === void 0 ? void 0 : config.physics) === null || _c === void 0 ? void 0 : _c.friction) !== null && _d !== void 0 ? _d : 0.98;
1195
1242
  const wind = (_f = (_e = config === null || config === void 0 ? void 0 : config.physics) === null || _e === void 0 ? void 0 : _e.wind) !== null && _f !== void 0 ? _f : 0;
1243
+ // Track frame count for mobile optimization
1244
+ let frameCount = 0;
1196
1245
  const updateParticles = () => {
1197
1246
  let activeParticles = 0;
1247
+ frameCount++;
1248
+ // Skip frame rendering on mobile to improve performance
1249
+ const skipFrame = shouldSkipFrame(frameCount);
1198
1250
  particlesRef.current = particlesRef.current.map((particle) => {
1199
1251
  if (particle.life <= 0)
1200
1252
  return particle;
@@ -1222,13 +1274,13 @@ const useReward = (elementId, animationType, config) => {
1222
1274
  }
1223
1275
  return particle;
1224
1276
  });
1225
- // Render particles
1226
- if (rootRef.current) {
1277
+ // Render particles (skip rendering on mobile for some frames)
1278
+ if (rootRef.current && !skipFrame) {
1227
1279
  rootRef.current.render(React.createElement(React.Fragment, null, particlesRef.current
1228
1280
  .filter((p) => p.life > 0)
1229
1281
  .map((particle) => (React.createElement("div", { key: particle.id, style: createParticleStyle(particle, containerRect) }, animationHandler.renderParticle(particle))))));
1230
1282
  }
1231
- if (activeParticles > 0) {
1283
+ if (activeParticles > 0 && isTabVisible.current) {
1232
1284
  animationFrameRef.current = requestAnimationFrame(updateParticles);
1233
1285
  }
1234
1286
  else {
@@ -1274,5 +1326,5 @@ const useReward = (elementId, animationType, config) => {
1274
1326
  return { reward, isAnimating };
1275
1327
  };
1276
1328
 
1277
- export { emojiPresets, useReward };
1329
+ export { emojiPresets, isMobileDevice, optimizeConfigForMobile, useReward };
1278
1330
  //# sourceMappingURL=index.esm.js.map