diff --git a/.gitignore b/.gitignore
index 26f7a3b..cdaf25a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,4 +85,3 @@ android/generated
# React Native Nitro Modules
nitrogen/
.playwright-cli/
-*.png
diff --git a/example/app.json b/example/app.json
index 466d381..0596f32 100644
--- a/example/app.json
+++ b/example/app.json
@@ -1,11 +1,15 @@
{
- "name": "EaseExample",
- "displayName": "EaseExample",
+ "name": "Ease",
+ "displayName": "Ease",
"expo": {
- "name": "EaseExample",
+ "name": "Ease",
"slug": "ease-example",
"scheme": "ease-example",
"version": "1.0.0",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "backgroundColor": "#1a1a2e"
+ },
"plugins": [
[
"expo-router",
@@ -16,7 +20,8 @@
],
"web": {
"bundler": "metro",
- "output": "single"
+ "output": "single",
+ "favicon": "./assets/favicon.png"
},
"platforms": [
"ios",
@@ -24,7 +29,11 @@
"web"
],
"android": {
- "package": "com.janic.easeexample"
+ "package": "com.janic.easeexample",
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#1a1a2e"
+ }
},
"ios": {
"bundleIdentifier": "com.janic.easeexample"
diff --git a/example/app/_layout.tsx b/example/app/_layout.tsx
index 191adf2..786cac5 100644
--- a/example/app/_layout.tsx
+++ b/example/app/_layout.tsx
@@ -1,17 +1,21 @@
import { Stack } from 'expo-router';
+import { MaxWidthContainer } from '../src/components/MaxWidthContainer';
+
export default function RootLayout() {
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/example/assets/adaptive-icon.png b/example/assets/adaptive-icon.png
new file mode 100644
index 0000000..6e026c1
Binary files /dev/null and b/example/assets/adaptive-icon.png differ
diff --git a/example/assets/favicon.png b/example/assets/favicon.png
new file mode 100644
index 0000000..aaf0f3c
Binary files /dev/null and b/example/assets/favicon.png differ
diff --git a/example/assets/icon.png b/example/assets/icon.png
new file mode 100644
index 0000000..e93ef0d
Binary files /dev/null and b/example/assets/icon.png differ
diff --git a/example/package.json b/example/package.json
index 26f4650..d3c45f1 100644
--- a/example/package.json
+++ b/example/package.json
@@ -10,7 +10,7 @@
"web": "expo start --web",
"prebuild": "expo prebuild",
"build:android": "expo prebuild --platform android --clean && cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a",
- "build:ios": "expo prebuild --platform ios --clean && xcodebuild -workspace ios/EaseExample.xcworkspace -scheme EaseExample -configuration Debug -sdk iphonesimulator -arch x86_64 build"
+ "build:ios": "expo prebuild --platform ios --clean && xcodebuild -workspace ios/Ease.xcworkspace -scheme Ease -configuration Debug -sdk iphonesimulator -arch x86_64 build"
},
"dependencies": {
"@expo/metro-runtime": "~55.0.6",
diff --git a/example/public/index.html b/example/public/index.html
new file mode 100644
index 0000000..5145852
--- /dev/null
+++ b/example/public/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ Ease
+
+
+
+
+
+
+
diff --git a/example/src/components/MaxWidthContainer.tsx b/example/src/components/MaxWidthContainer.tsx
new file mode 100644
index 0000000..2caaf03
--- /dev/null
+++ b/example/src/components/MaxWidthContainer.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+
+const MAX_WIDTH = 600;
+
+export function MaxWidthContainer({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ inner: {
+ flex: 1,
+ width: '100%',
+ maxWidth: MAX_WIDTH,
+ },
+});
diff --git a/package.json b/package.json
index 6066688..9f52fd2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-ease",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "Lightweight declarative animations powered by platform APIs",
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",
diff --git a/src/EaseView.web.tsx b/src/EaseView.web.tsx
index 5c4d8b9..1592df9 100644
--- a/src/EaseView.web.tsx
+++ b/src/EaseView.web.tsx
@@ -30,6 +30,80 @@ const EASING_PRESETS: Record = {
easeInOut: [0.42, 0, 0.58, 1],
};
+// ---------------------------------------------------------------------------
+// Spring simulation → CSS linear() easing
+// ---------------------------------------------------------------------------
+
+/** Simulate a damped spring from 0 → 1 and return settling duration + sample points. */
+function simulateSpring(
+ damping: number,
+ stiffness: number,
+ mass: number,
+): { durationMs: number; points: number[] } {
+ const dt = 1 / 120; // 120 Hz simulation
+ const maxTime = 10; // 10s safety cap
+ let x = 0;
+ let v = 0;
+ const samples: number[] = [0];
+ let step = 0;
+
+ while (step * dt < maxTime) {
+ const a = (-stiffness * (x - 1) - damping * v) / mass;
+ v += a * dt;
+ x += v * dt;
+ step++;
+ // Downsample to ~60 fps (every 2nd sample)
+ if (step % 2 === 0) {
+ samples.push(Math.round(x * 10000) / 10000);
+ }
+ // Settled?
+ if (Math.abs(x - 1) < 0.001 && Math.abs(v) < 0.001) break;
+ }
+
+ // Ensure last point is exactly 1
+ samples[samples.length - 1] = 1;
+
+ return {
+ durationMs: Math.round(step * dt * 1000),
+ points: samples,
+ };
+}
+
+/** Cache for computed spring easing strings (keyed by damping-stiffness-mass). */
+const springCache = new Map();
+
+function getSpringEasing(
+ damping: number,
+ stiffness: number,
+ mass: number,
+): { duration: number; easing: string } {
+ const key = `${damping}-${stiffness}-${mass}`;
+ let cached = springCache.get(key);
+ if (cached) return cached;
+
+ const { durationMs, points } = simulateSpring(damping, stiffness, mass);
+ const easing = `linear(${points.join(', ')})`;
+ cached = { duration: durationMs, easing };
+ springCache.set(key, cached);
+ return cached;
+}
+
+/** Detect CSS linear() support (lazy, cached). */
+let linearSupported: boolean | null = null;
+function supportsLinearEasing(): boolean {
+ if (linearSupported != null) return linearSupported;
+ try {
+ const el = document.createElement('div');
+ el.style.transitionTimingFunction = 'linear(0, 1)';
+ linearSupported = el.style.transitionTimingFunction !== '';
+ } catch {
+ linearSupported = false;
+ }
+ return linearSupported;
+}
+
+const SPRING_FALLBACK_EASING = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
+
export type EaseViewProps = {
animate?: AnimateProps;
initialAnimate?: AnimateProps;
@@ -154,9 +228,19 @@ function resolvePerCategoryConfigs(
}
function resolveEasing(transition: SingleTransition | undefined): string {
- if (!transition || transition.type !== 'timing') {
- return 'cubic-bezier(0.42, 0, 0.58, 1)';
+ if (!transition || transition.type === 'none') {
+ return 'linear';
+ }
+ if (transition.type === 'spring') {
+ const d = transition.damping ?? 15;
+ const s = transition.stiffness ?? 120;
+ const m = transition.mass ?? 1;
+ if (supportsLinearEasing()) {
+ return getSpringEasing(d, s, m).easing;
+ }
+ return SPRING_FALLBACK_EASING;
}
+ // timing
const easing = transition.easing ?? 'easeInOut';
const bezier: CubicBezier = Array.isArray(easing)
? easing
@@ -168,10 +252,11 @@ function resolveDuration(transition: SingleTransition | undefined): number {
if (!transition) return 300;
if (transition.type === 'timing') return transition.duration ?? 300;
if (transition.type === 'none') return 0;
- const damping = transition.damping ?? 15;
- const mass = transition.mass ?? 1;
- const tau = (2 * mass) / damping;
- return Math.round(tau * 4 * 1000);
+ // Spring: use simulation-derived duration (incorporates stiffness)
+ const d = transition.damping ?? 15;
+ const s = transition.stiffness ?? 120;
+ const m = transition.mass ?? 1;
+ return getSpringEasing(d, s, m).duration;
}
/** Counter for unique keyframe names. */
@@ -237,13 +322,7 @@ export function EaseView({
})
.map((key) => {
const cfg = categoryConfigs[key];
- const springEasing =
- cfg.type === 'spring'
- ? 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
- : null;
- return `${CSS_PROP_MAP[key]} ${cfg.duration}ms ${
- springEasing ?? cfg.easing
- }`;
+ return `${CSS_PROP_MAP[key]} ${cfg.duration}ms ${cfg.easing}`;
})
.join(', ') || 'none';