test: validate release package with real WeatherV2
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-09 14:29:10 +08:00
parent fa431ba6fd
commit 3ac678a3c8
16 changed files with 352 additions and 34 deletions

View File

@@ -1,26 +1,87 @@
import { useCallback, useState } from "react";
import { Button, SafeAreaView, StyleSheet, Text, View, findNodeHandle } from "react-native";
import { File, Paths } from "expo-file-system";
import { Camera, Map, WeatherRuntime, createParticleWeatherStyle } from "@navsea/maplibre-particle-rn";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
import { Camera, Map, WeatherRuntime, type ColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn";
import { materializeRealWeatherFrames, realWeatherFrames } from "./src/realWeatherDataset";
import { weatherTestStyle } from "./src/weatherStyle";
const metadata = (generation: number, validTime: number) => JSON.stringify({ schema: "maplibre-particle-field/v1", fieldId: `consumer-smoke-${generation}`, fieldType: "WIND", displayDomain: "GLOBAL", generation, validTime, dataVersion: generation, bounds: { west: -180, south: -85, east: 180, north: 85 }, grid: { width: 2, height: 2 }, vector: { layout: "interleaved-uv", encoding: "float32-le", noData: -9999 }, payload: { byteLength: 32 } });
function writeFixture() {
const bytes = new Uint8Array(32); const view = new DataView(bytes.buffer);
for (let offset = 0; offset < bytes.byteLength; offset += 8) { view.setFloat32(offset, 5, true); view.setFloat32(offset + 4, 1, true); }
const file = new File(Paths.cache, "navsea-particle-consumer-smoke.f32"); file.write(bytes); return file.uri;
}
const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false };
const initialVisual = { count: "4096", width: "1.5", length: "0.012", opacity: "0.9", trailOpacity: "0.8", trailFade: "0.94", weatherOpacity: "0.62" };
const frozenStops: ColorStop[] = [
{ speedMps: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { speedMps: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 },
{ speedMps: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { speedMps: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 },
];
export default function App() {
const [tag, setTag] = useState<number | null>(null); const [state, setState] = useState("STYLE_LOADING");
const onStyle = useCallback(async (event: unknown) => {
const value = findNodeHandle((event as { target?: unknown }).target as never); if (value == null) return;
try { await WeatherRuntime.attach(value); setTag(value); setState("WAITING_FOR_FIELDS"); } catch (error) { setState(String(error)); }
}, []);
const submit = useCallback(async () => {
if (tag == null) return;
try { await WeatherRuntime.clearWindFields(tag); const payload = writeFixture(); const t0 = await WeatherRuntime.submitWindField(tag, metadata(1, 1786060800000), payload); const t1 = await WeatherRuntime.submitWindField(tag, metadata(2, 1786082400000), payload); const runtime = await WeatherRuntime.getState(tag); setState(`T0 ${t0} / T1 ${t1} / ${runtime.state}`); } catch (error) { setState(String(error)); }
}, [tag]);
return <SafeAreaView style={styles.root}><View style={styles.map}><Map mapStyle={createParticleWeatherStyle()} onDidFinishLoadingStyle={onStyle}><Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} /></Map></View><View style={styles.controls}><Text>Package smoke: {state}</Text><Button title="Submit canonical fixture" onPress={() => void submit()} /></View></SafeAreaView>;
const [reactTag, setReactTag] = useState<number | null>(null);
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
const [result, setResult] = useState("Style is loading");
const [basemap, setBasemap] = useState("LOADING");
const [frames, setFrames] = useState<Record<string, string>>({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" });
const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
const [weatherVisible, setWeatherVisible] = useState(true);
const [particleVisible, setParticleVisible] = useState(true);
const [visual, setVisual] = useState(initialVisual);
const refresh = useCallback(async (tag: number) => setSnapshot(await WeatherRuntime.getState(tag)), []);
const attach = useCallback(async (tag: number) => {
try { await WeatherRuntime.attach(tag); await refresh(tag); setReactTag(tag); setResult("WeatherRuntime attached; style ready"); }
catch (error) { setResult(`style/attach error: ${String(error)}`); }
}, [refresh]);
const onStyleReady = useCallback((event: unknown) => {
const tag = findNodeHandle((event as { target?: unknown }).target as never);
if (tag != null) void attach(tag);
}, [attach]);
useEffect(() => () => { if (reactTag != null) void WeatherRuntime.detach(reactTag).catch(() => undefined); }, [reactTag]);
const command = useCallback(async (name: string, operation: (tag: number) => Promise<unknown>) => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
try { const value = await operation(reactTag); await refresh(reactTag); setResult(`${name}: ${String(value)}`); }
catch (error) { setResult(`${name}: ${String(error)}`); }
}, [reactTag, refresh]);
const loadRealWeather = useCallback(async () => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY", "ACCEPTED_NEXT_SNAPSHOT", "ACCEPTED_NEXT_SNAPSHOT"];
try {
setTransport("LOADING: materializing Expo Asset local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" });
if (await WeatherRuntime.clearWindFields(reactTag) !== "ACCEPTED") throw new Error("clearWindFields was rejected");
const localFrames = await materializeRealWeatherFrames();
for (let index = 0; index < localFrames.length; index += 1) {
const frame = localFrames[index]; const submit = await WeatherRuntime.submitWindField(reactTag, frame.metadataJson, frame.payloadFileUri);
setFrames((current) => ({ ...current, [frame.label]: submit }));
if (submit !== expected[index]) throw new Error(`${frame.label} expected ${expected[index]}, got ${submit}`);
}
const styles = StyleSheet.create({ root: { flex: 1 }, map: { flex: 1 }, controls: { padding: 12, backgroundColor: "white" } });
const native = await WeatherRuntime.getState(reactTag); setSnapshot(native);
if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`);
setTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); setResult("T0-T3 accepted in strict producer timestamp order; Native runtime READY");
} catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD REAL WEATHER T0-T3: ${String(error)}`); }
}, [reactTag]);
const updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []);
const applyVisual = useCallback(() => void command("particle visual", async (tag) => {
const results = await Promise.all([
WeatherRuntime.setParticleCount(tag, Number(visual.count)), WeatherRuntime.setParticleLineWidth(tag, Number(visual.width)), WeatherRuntime.setParticleLength(tag, Number(visual.length)), WeatherRuntime.setParticleOpacity(tag, Number(visual.opacity)), WeatherRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)), WeatherRuntime.setTrailFade(tag, Number(visual.trailFade)), WeatherRuntime.setWeatherColorOpacity(tag, Number(visual.weatherOpacity)),
]);
if (results.some((value) => value !== "ACCEPTED")) throw new Error(results.join(", ")); return "ACCEPTED";
}), [command, visual]);
const controls = useMemo(() => [
["PLAY", (tag: number) => WeatherRuntime.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => WeatherRuntime.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => WeatherRuntime.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => WeatherRuntime.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => WeatherRuntime.setPlaybackSpeed(tag, "DOUBLE")],
["FPS AUTO", (tag: number) => WeatherRuntime.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => WeatherRuntime.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => WeatherRuntime.setTargetFps(tag, "FPS_60" as TargetFps)], ["Weather stops", (tag: number) => WeatherRuntime.setWeatherColorStops(tag, frozenStops)], ["Particle color", (tag: number) => WeatherRuntime.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => WeatherRuntime.setTrailColor(tag, 0.35, 0.85, 1, 0.8)],
] as const, []);
return <SafeAreaView style={styles.root}>
<View style={styles.map}><Map mapStyle={weatherTestStyle} onDidFinishLoadingStyle={onStyleReady} onDidFinishLoadingMap={() => setBasemap("READY")} onDidFailLoadingMap={() => { setBasemap("ERROR"); setResult("map loading error"); }}><Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} /></Map></View>
<ScrollView style={styles.controls} contentContainerStyle={styles.content}>
<Text>Basemap: GSI {basemap}</Text><Text>Dataset: ECMWF HRES 20260807 · 513×769 · 3.01 MiB/frame</Text><Text>Runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text><Text selectable>REAL DATA TRANSPORT = {transport}</Text>
{realWeatherFrames.map((frame) => <Text selectable key={frame.label}>{frame.label} {frame.cycle}: {frames[frame.label]}</Text>)}
<Text selectable>{result}</Text><Button title="LOAD REAL WEATHER T0-T3" onPress={() => void loadRealWeather()} />
<View style={styles.row}>{controls.map(([label, operation]) => <Button key={label} title={label} onPress={() => void command(label, operation)} />)}</View>
<View style={styles.row}><Button title={`Weather ${weatherVisible ? "visible" : "hidden"}`} onPress={() => { const next = !weatherVisible; setWeatherVisible(next); void command("weather visible", (tag) => WeatherRuntime.setWeatherColorVisible(tag, next)); }} /><Button title={`Particle ${particleVisible ? "visible" : "hidden"}`} onPress={() => { const next = !particleVisible; setParticleVisible(next); void command("particle visible", (tag) => WeatherRuntime.setParticleVisible(tag, next)); }} /></View>
<View style={styles.row}>{realWeatherFrames.map((frame) => <Button key={`seek-${frame.label}`} title={`seek ${frame.label}`} onPress={() => void command(`seek ${frame.label}`, (tag) => WeatherRuntime.setForecastTime(tag, frame.validTime))} />)}</View>
<Text>Visual: count / width / length / particle opacity / trail opacity / trail fade / WeatherColor opacity</Text><View style={styles.row}>{(Object.keys(visual) as Array<keyof typeof initialVisual>).map((key) => <TextInput key={key} value={visual[key]} onChangeText={(value) => updateVisual(key, value)} keyboardType="decimal-pad" style={styles.input} />)}<Button title="Apply visual" onPress={applyVisual} /></View>
</ScrollView>
</SafeAreaView>;
}
const styles = StyleSheet.create({ root: { flex: 1 }, map: { flex: 1 }, controls: { maxHeight: 310, backgroundColor: "#fff" }, content: { gap: 7, padding: 10 }, row: { flexDirection: "row", flexWrap: "wrap", gap: 6, alignItems: "center" }, input: { borderWidth: 1, minWidth: 48, padding: 5 } });

View File

@@ -1,14 +1,9 @@
# MapLibreParticlePackageConsumerTestHost
这是发布包 Consumer smoke host,不引用 `packages/`、Test Host bridge 源码、MapLibre Native source tree 或 workspace dependency。其 `package.json` 只直接依赖 `@navsea/maplibre-particle-rn@13.4.1-particle.1`;Map、Camera、WeatherRuntime 和 style helper 全部从该发布包导入。
这是正式发布包 Consumer 验收 Host。它用于模拟 NavSea 的升级方式:Consumer 只直接安装 `@navsea/maplibre-particle-rn@13.4.1-particle.1`,由该 npm 包接入 React Native,并由 Gradle 从 `https://pbf.jpcn.ccwu.cc/maven` 解析正式 Particle AAR。
最终 registry 未决定时,先在包目录生成 tarball,再在本目录手工执行:
禁止直接依赖 `@maplibre/maplibre-react-native`、旧 `weather-runtime-bridge`、MapLibre Native source tree、Maven Local、本地 AAR 或 workspace/source-path bridge。Map、Camera、WeatherRuntime 统一从 `@navsea/maplibre-particle-rn` 导入。
```bash
npm install /absolute/path/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz --save-exact
npm install --ignore-scripts
npx expo prebuild --platform android --clean
npx expo run:android
```
当前验收数据复用 STEP-062 已通过的真实 WeatherV2 fixture:ECMWF HRES 20260807,513×769,T0/T1/T2/T3 四个 canonical `float32-le / interleaved-uv` 风场。`.f32` 通过 Expo Asset materialize 为本地 `file://` URI 后提交给 WeatherRuntime,payload 不进入 JS 数组。
该 `.tgz` 是发布物而非源码路径。Android 正式 artifact 已发布到 `https://pbf.jpcn.ccwu.cc/maven`;package plugin 会自动加入该仓库,并将 MLRN 的唯一 OpenGL SDK 请求替换为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。
验收目标是同时看到 GSI 底图、WeatherColor 与 Particle,并确认 T0~T3 顺序接受、Runtime 进入 READY。这样验证的是“正式 npm 包 → 正式 Maven 仓库 → 正式 AAR → RN/Native/Core”的完整升级链,而不是源码工程或开发期 AAR。

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080700",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786060800000,
"validTime": 1786060800000,
"dataVersion": 1786060800000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080700",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080706",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786082400000,
"validTime": 1786082400000,
"dataVersion": 1786082400000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080706",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080712",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786104000000,
"validTime": 1786104000000,
"dataVersion": 1786104000000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "d86a6a51fd5216cf9368756962a5366fd32a627afd40cfde235ba7358c02c5a0"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080712",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080718",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786125600000,
"validTime": 1786125600000,
"dataVersion": 1786125600000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "970785b0334a9d1511b8897874a81f681814a32b1e0b90e37cf64b77ca9fa39b"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080718",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,14 @@
{
"schema": "maplibre-particle-test-dataset/v1",
"datasetId": "ecmwf-hres-wind-20260807",
"source": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"validTimes": [
"2026080700",
"2026080706",
"2026080712",
"2026080718"
],
"snapshotLayout": "<validTime>/metadata.json + <validTime>/field.f32",
"notes": "开发期离线 canonical 测试索引;不是 Weather Manifest,也不进入 ParticleFieldSnapshot。"
}

View File

@@ -0,0 +1,6 @@
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
config.resolver.assetExts = [...config.resolver.assetExts, "f32"];
module.exports = config;

View File

@@ -13,7 +13,8 @@
"expo": "56.0.4",
"expo-file-system": "56.0.11",
"react": "19.2.3",
"react-native": "0.85.2"
"react-native": "0.85.2",
"expo-asset": "~56.0.14"
},
"devDependencies": {
"@types/react": "19.2.5",

View File

@@ -0,0 +1,40 @@
import { Asset } from "expo-asset";
export type RealWeatherFrame = Readonly<{
label: "T0" | "T1" | "T2" | "T3";
cycle: "2026080700" | "2026080706" | "2026080712" | "2026080718";
metadataJson: string;
validTime: number;
generation: number;
payloadModule: number;
}>;
type CanonicalMetadata = Readonly<{ validTime: number; generation: number }>;
// Static requires register all four raw .f32 assets with Metro/Expo; payload bytes never enter JS memory.
const metadataT0 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080700/metadata.json") as CanonicalMetadata;
const metadataT1 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080706/metadata.json") as CanonicalMetadata;
const metadataT2 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080712/metadata.json") as CanonicalMetadata;
const metadataT3 = require("../assets/weather/ecmwf-hres-wind-20260807/2026080718/metadata.json") as CanonicalMetadata;
function frame(label: RealWeatherFrame["label"], cycle: RealWeatherFrame["cycle"], metadata: CanonicalMetadata, payloadModule: number): RealWeatherFrame {
return { label, cycle, metadataJson: JSON.stringify(metadata), validTime: metadata.validTime, generation: metadata.generation, payloadModule };
}
export const realWeatherFrames = [
frame("T0", "2026080700", metadataT0, require("../assets/weather/ecmwf-hres-wind-20260807/2026080700/field.f32")),
frame("T1", "2026080706", metadataT1, require("../assets/weather/ecmwf-hres-wind-20260807/2026080706/field.f32")),
frame("T2", "2026080712", metadataT2, require("../assets/weather/ecmwf-hres-wind-20260807/2026080712/field.f32")),
frame("T3", "2026080718", metadataT3, require("../assets/weather/ecmwf-hres-wind-20260807/2026080718/field.f32")),
] as const;
export async function materializeRealWeatherFrames(): Promise<ReadonlyArray<RealWeatherFrame & { payloadFileUri: string }>> {
return Promise.all(realWeatherFrames.map(async (weatherFrame) => {
const asset = Asset.fromModule(weatherFrame.payloadModule);
await asset.downloadAsync();
if (asset.localUri == null || !asset.localUri.startsWith("file://")) {
throw new Error(`${weatherFrame.label} Asset.downloadAsync did not provide a local file:// URI`);
}
return { ...weatherFrame, payloadFileUri: asset.localUri };
}));
}

View File

@@ -0,0 +1,31 @@
/** No field is embedded here: RasterArraySource is reserved for STEP-061 file-reference submission. */
export const WEATHER_SOURCE_ID = "rn-weather-fields";
export const WEATHER_COLOR_LAYER_ID = "rn-weather-color";
export const PARTICLE_LAYER_ID = "rn-weather-particles";
export const GSI_SOURCE_ID = "gsi-standard";
export const GSI_LAYER_ID = "gsi-standard";
export const weatherTestStyle = JSON.stringify({
version: 8,
sources: {
[GSI_SOURCE_ID]: {
type: "raster",
tiles: ["https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"],
tileSize: 256,
maxzoom: 18,
attribution: "\u56fd\u571f\u5730\u7406\u9662 / GSI"
},
[WEATHER_SOURCE_ID]: { type: "raster-array", "host-field-submission": true }
},
layers: [
{ id: "rn-basemap", type: "background", paint: { "background-color": "#10243a" } },
{ id: GSI_LAYER_ID, type: "raster", source: GSI_SOURCE_ID, paint: { "raster-opacity": 1 } },
{ id: WEATHER_COLOR_LAYER_ID, type: "weather-color", source: WEATHER_SOURCE_ID,
paint: { "weather-color-opacity": 0.62 } },
{ id: PARTICLE_LAYER_ID, type: "raster-particle", source: WEATHER_SOURCE_ID,
paint: { "raster-particle-active": true, "raster-particle-particle-count": 4096,
"raster-particle-line-width": 1.5, "raster-particle-particle-length": 0.012,
"raster-particle-particle-opacity": 0.9, "raster-particle-trail-opacity": 0.8,
"raster-particle-trail-fade": 0.94 } }
]
});

View File

@@ -1,5 +1,11 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": { "strict": true, "noEmit": true },
"include": ["App.tsx"]
"compilerOptions": {
"strict": true,
"noEmit": true
},
"include": [
"App.tsx",
"src/**/*.ts"
]
}