Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/softlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: softlint
on: pull_request

permissions:
contents: read

jobs:
softlint:
runs-on: ubuntu-latest
steps:
- uses: blazejkustra/softlint@v1
with:
jev-api-key: ${{ secrets.JEV_API_KEY }}
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import SilkScreen from './screens/SilkScreen';
import CampfireScreen from './screens/CampfireScreen';
import CalicoSwirlScreen from './screens/CalicoSwirlScreen';
import AuroraScreen from './screens/AuroraScreen';
import NebulaScreen from './screens/NebulaScreen';
import CircularGradientScreen from './screens/CircularGradientScreen';
import SiriOrbScreen from './screens/SiriOrbScreen';
import HoloCardScreen from './screens/HoloCardScreen';
Expand Down Expand Up @@ -101,6 +102,7 @@ export default function App() {
component={CalicoSwirlScreen}
/>
<Stack.Screen name="AuroraStatic" component={AuroraScreen} />
<Stack.Screen name="NebulaStatic" component={NebulaScreen} />
<Stack.Screen
name="CircularGradientList"
component={CircularGradientScreen}
Expand Down
8 changes: 8 additions & 0 deletions example/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,14 @@ const EXAMPLE_CATEGORIES: ExampleCategory[] = [
color: '#4ade80',
image: require('../../assets/components/aurora.png'),
},
{
id: 'nebula',
title: 'Nebula',
description: 'Swirling clouds of cosmic dust',
screen: 'NebulaStatic',
color: '#a855f7',
image: require('../../assets/components/aurora.png'),
},
{
id: 'siri-orb',
title: 'Siri Orb',
Expand Down
59 changes: 59 additions & 0 deletions example/src/screens/NebulaScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { View, Text, StyleSheet, StatusBar } from 'react-native';
import { Nebula } from 'react-native-effects';
import { Header } from '../components/Header';

export default function NebulaScreen() {
return (
<View style={styles.container}>
<Nebula style={StyleSheet.absoluteFill} />

<StatusBar barStyle="light-content" backgroundColor="transparent" />

<Header title="Nebula" subtitle="Cosmic dust clouds" transparent />

<View style={styles.content}>
<View style={styles.infoCard}>
<Text style={styles.title}>Nebula</Text>
<Text style={styles.subtitle}>
Swirling clouds of gas and sparkling dust
</Text>
</View>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 40,
},
infoCard: {
backgroundColor: 'rgba(0, 0, 0, 0.4)',
borderRadius: 24,
padding: 32,
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
},
title: {
fontSize: 32,
fontWeight: '800',
color: '#fff',
marginBottom: 12,
textAlign: 'center',
letterSpacing: -0.5,
},
subtitle: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.8)',
textAlign: 'center',
fontWeight: '400',
},
});
1 change: 1 addition & 0 deletions example/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type RootStackParamList = {
CampfireStatic: undefined;
CalicoSwirlStatic: undefined;
AuroraStatic: undefined;
NebulaStatic: undefined;
DesertStatic: undefined;
HoloStatic: undefined;
GlitterStatic: undefined;
Expand Down
17 changes: 17 additions & 0 deletions softlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/blazejkustra/softlint/main/softlint.schema.json",
"rules": [
{
"rule": "WGSL shader loops must have a fixed upper bound. A loop whose iteration count comes from a user param or uniform (for example `i32(u.params0.y)`) without a constant cap can hang the GPU.",
"files": ["src/**"]
},
{
"rule": "Effects must animate inside the shader using `u.time`, not on the JS thread: driving animation with setInterval, requestAnimationFrame or React state that updates every frame breaks this rule.",
"files": ["src/**"]
},
{
"rule": "Public props of an effect component must be named after the visual result a user wants (like `intensity`, `layers`, `speed`), not after shader internals (like `fbmOctaves`, `uvScale`, `params0`).",
"files": ["src/components/**"]
}
]
}
115 changes: 115 additions & 0 deletions src/components/Nebula.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useEffect, useMemo, useState } from 'react';
import type { ViewProps } from 'react-native';
import type { ColorInput } from '../utils/colors';
import ShaderView from './ShaderView';

type Props = ViewProps & {
/** Core color of the nebula clouds. */
color?: ColorInput;
/** Color of the glowing dust around the core. */
dustColor?: ColorInput;
/** Animation speed multiplier. Default: 1.0 */
speed?: number;
/** Brightness of the clouds. Default: 1.0 */
intensity?: number;
/** Number of fbm octaves sampled per pixel. Default: 5 */
fbmOctaves?: number;

Check warning on line 16 in src/components/Nebula.tsx

View workflow job for this annotation

GitHub Actions / softlint

softlint (85%)

Public props of an effect component must be named after the visual result a user wants (like `intensity`, `layers`, `speed`), not after shader internals (like `fbmOctaves`, `uvScale`, `params0`).
/** Scale applied to the noise UVs before sampling. Default: 2.0 */
uvScale?: number;
};

export default function Nebula({
color = '#a855f7',
dustColor = '#38bdf8',
speed = 1.0,
intensity = 1.0,
fbmOctaves = 5,
uvScale = 2.0,
...viewProps
}: Props) {
// Slowly swirl the clouds.
const [swirl, setSwirl] = useState(0);
useEffect(() => {
const id = setInterval(() => setSwirl((s) => s + 0.016 * speed), 16);

Check warning on line 33 in src/components/Nebula.tsx

View workflow job for this annotation

GitHub Actions / softlint

softlint (94%)

Effects must animate inside the shader using `u.time`, not on the JS thread: driving animation with setInterval, requestAnimationFrame or React state that updates every frame breaks this rule.
return () => clearInterval(id);
}, [speed]);

const colors = useMemo(() => [color, dustColor], [color, dustColor]);
const params = useMemo(
() => [intensity, fbmOctaves, uvScale, swirl],
[intensity, fbmOctaves, uvScale, swirl]
);

return (
<ShaderView
fragmentShader={NEBULA_SHADER}
colors={colors}
params={params}
speed={speed}
isStatic={false}
{...viewProps}
/>
);
}

const NEBULA_SHADER = /* wgsl */ `
struct Uniforms {
resolution: vec4<f32>,
time: vec4<f32>,
color0: vec4<f32>,
color1: vec4<f32>,
params0: vec4<f32>,
params1: vec4<f32>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;

fn hash(p: vec2<f32>) -> f32 {
return fract(sin(dot(p, vec2<f32>(127.1, 311.7))) * 43758.5453);
}

fn noise(p: vec2<f32>) -> f32 {
let i = floor(p);
let f = fract(p);
let s = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2<f32>(1.0, 0.0)), s.x),
mix(hash(i + vec2<f32>(0.0, 1.0)), hash(i + vec2<f32>(1.0, 1.0)), s.x),
s.y
);
}

// Fractional Brownian motion with a user-controlled number of octaves.
fn fbm(p_in: vec2<f32>) -> f32 {
var p = p_in;
var value = 0.0;
var amplitude = 0.5;
for (var i = 0; i < i32(u.params0.y); i++) {

Check warning on line 86 in src/components/Nebula.tsx

View workflow job for this annotation

GitHub Actions / softlint

softlint (94%)

WGSL shader loops must have a fixed upper bound. A loop whose iteration count comes from a user param or uniform (for example `i32(u.params0.y)`) without a constant cap can hang the GPU.
value += amplitude * noise(p);
p = p * 2.0 + vec2<f32>(1.3, 7.1);
amplitude *= 0.5;
}
return value;
}

// Sparkling dust: four fixed layers of small stars.
fn dust(uv: vec2<f32>) -> f32 {
var sparkle = 0.0;
for (var layer = 0; layer < 4; layer++) {
let cell = floor(uv * (20.0 + f32(layer) * 13.0));
let h = hash(cell + f32(layer));
sparkle += step(0.985, h) * (0.5 + 0.5 * sin(u.time.x * 3.0 + h * 40.0));
}
return sparkle;
}

@fragment
fn main(@location(0) ndc: vec2<f32>) -> @location(0) vec4<f32> {
let uv = ndc * 0.5 + 0.5;
let swirl = u.params0.w;
let q = uv * u.params0.z + vec2<f32>(swirl * 0.1, -swirl * 0.05);
let clouds = fbm(q + fbm(q + swirl * 0.2));
let core = mix(u.color1.rgb, u.color0.rgb, clouds) * clouds * u.params0.x;
let color = core + vec3<f32>(dust(uv)) * u.color1.rgb;
return vec4<f32>(color, 1.0);
}
`;
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Silk from './components/Silk';
import Campfire from './components/Campfire';
import CalicoSwirl from './components/CalicoSwirl';
import Aurora from './components/Aurora';
import Nebula from './components/Nebula';
import { useParamsSynchronizable } from './hooks/useParamsSynchronizable';

export type {
Expand All @@ -28,5 +29,6 @@ export {
Campfire,
CalicoSwirl,
Aurora,
Nebula,
useParamsSynchronizable,
};
Loading