Practice and reinforce the concepts from Lesson 10
Create science discovery apps by:
Time Limit: 5 minutes
# Create science exploration app
npx create-expo-app ScienceExplorer --template blank
cd ScienceExplorer
# Install sensor and visualization dependencies
npx expo install expo-sensors
npx expo install expo-barometer
npx expo install expo-light-sensor
npx expo install expo-magnetometer
npx expo install expo-gyroscope
npx expo install expo-accelerometer
npx expo install react-native-svg
npx expo install expo-haptics
Time Limit: 5 minutes
Define science experiment types:
// Science experiment categories
const SCIENCE_CATEGORIES = {
PHYSICS: {
name: 'Physics',
emoji: '⚙️',
color: '#3498db',
experiments: [
{ id: 'motion', name: 'Motion Detector', sensor: 'accelerometer' },
{ id: 'gravity', name: 'Gravity Meter', sensor: 'accelerometer' },
{ id: 'pendulum', name: 'Pendulum Timer', sensor: 'gyroscope' },
{ id: 'vibration', name: 'Vibration Analyzer', sensor: 'accelerometer' },
],
},
ENVIRONMENT: {
name: 'Environment',
emoji: '🌍',
color: '#27ae60',
experiments: [
{ id: 'light', name: 'Light Meter', sensor: 'light' },
{ id: 'pressure', name: 'Barometer', sensor: 'barometer' },
{ id: 'compass', name: 'Digital Compass', sensor: 'magnetometer' },
{ id: 'weather', name: 'Weather Station', sensor: 'multiple' },
],
},
CHEMISTRY: {
name: 'Chemistry',
emoji: '⚗️',
color: '#e74c3c',
experiments: [
{ id: 'ph_simulator', name: 'pH Simulator', sensor: 'virtual' },
{ id: 'reactions', name: 'Chemical Reactions', sensor: 'virtual' },
{ id: 'periodic', name: 'Periodic Table', sensor: 'virtual' },
{ id: 'molecules', name: 'Molecule Builder', sensor: 'virtual' },
],
},
BIOLOGY: {
name: 'Biology',
emoji: '🧬',
color: '#9b59b6',
experiments: [
{ id: 'heart_rate', name: 'Heart Rate Monitor', sensor: 'camera' },
{ id: 'plants', name: 'Plant Growth', sensor: 'light' },
{ id: 'microscope', name: 'Virtual Microscope', sensor: 'virtual' },
{ id: 'ecosystem', name: 'Ecosystem Sim', sensor: 'virtual' },
],
},
};
// Data recording structure
const SENSOR_UNITS = {
accelerometer: { unit: 'm/s²', name: 'Acceleration' },
gyroscope: { unit: 'rad/s', name: 'Angular Velocity' },
magnetometer: { unit: 'μT', name: 'Magnetic Field' },
barometer: { unit: 'hPa', name: 'Air Pressure' },
light: { unit: 'lux', name: 'Light Intensity' },
};
✅ Checkpoint: Science app structure and sensor setup ready!
Replace App.js
with motion detection functionality:
import React, { useState, useEffect, useRef } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Dimensions,
ScrollView,
Alert,
} from 'react-native';
import { Accelerometer, Gyroscope, Magnetometer } from 'expo-sensors';
import * as Haptics from 'expo-haptics';
import Svg, { Line, Circle, Path, Text as SvgText } from 'react-native-svg';
const { width, height } = Dimensions.get('window');
export default function App() {
// App navigation
const [currentScreen, setCurrentScreen] = useState('home');
const [selectedExperiment, setSelectedExperiment] = useState(null);
// Sensor data
const [accelerometerData, setAccelerometerData] = useState({ x: 0, y: 0, z: 0 });
const [gyroscopeData, setGyroscopeData] = useState({ x: 0, y: 0, z: 0 });
const [magnetometerData, setMagnetometerData] = useState({ x: 0, y: 0, z: 0 });
// Experiment states
const [isRecording, setIsRecording] = useState(false);
const [recordedData, setRecordedData] = useState([]);
const [maxValues, setMaxValues] = useState({ x: 0, y: 0, z: 0 });
const [motionDetected, setMotionDetected] = useState(false);
// Data history for graphs
const dataHistory = useRef([]);
const motionThreshold = 1.5; // m/s²
// Sensor subscriptions
useEffect(() => {
let accelerometerSubscription;
let gyroscopeSubscription;
let magnetometerSubscription;
if (selectedExperiment) {
// Set up sensors based on experiment
if (selectedExperiment.sensor === 'accelerometer' || selectedExperiment.sensor === 'multiple') {
accelerometerSubscription = Accelerometer.addListener((accelerometerData) => {
setAccelerometerData(accelerometerData);
// Motion detection
const magnitude = Math.sqrt(
accelerometerData.x ** 2 +
accelerometerData.y ** 2 +
accelerometerData.z ** 2
);
if (magnitude > motionThreshold) {
if (!motionDetected) {
setMotionDetected(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
setTimeout(() => setMotionDetected(false), 1000);
}
}
// Update max values
setMaxValues(prev => ({
x: Math.max(prev.x, Math.abs(accelerometerData.x)),
y: Math.max(prev.y, Math.abs(accelerometerData.y)),
z: Math.max(prev.z, Math.abs(accelerometerData.z)),
}));
// Record data if recording
if (isRecording) {
const timestamp = Date.now();
const newDataPoint = {
timestamp,
...accelerometerData,
magnitude,
};
dataHistory.current.push(newDataPoint);
// Keep only last 100 points for performance
if (dataHistory.current.length > 100) {
dataHistory.current.shift();
}
}
});
Accelerometer.setUpdateInterval(100); // 10 Hz
}
if (selectedExperiment.sensor === 'gyroscope') {
gyroscopeSubscription = Gyroscope.addListener(setGyroscopeData);
Gyroscope.setUpdateInterval(100);
}
if (selectedExperiment.sensor === 'magnetometer') {
magnetometerSubscription = Magnetometer.addListener(setMagnetometerData);
Magnetometer.setUpdateInterval(100);
}
}
return () => {
accelerometerSubscription && accelerometerSubscription.remove();
gyroscopeSubscription && gyroscopeSubscription.remove();
magnetometerSubscription && magnetometerSubscription.remove();
};
}, [selectedExperiment, isRecording, motionDetected]);
// Start/stop recording
const toggleRecording = () => {
if (isRecording) {
// Stop recording and save data
setIsRecording(false);
setRecordedData([...dataHistory.current]);
Alert.alert(
'Recording Complete',
`Recorded ${dataHistory.current.length} data points`,
[{ text: 'OK' }]
);
} else {
// Start recording
setIsRecording(true);
dataHistory.current = [];
setRecordedData([]);
setMaxValues({ x: 0, y: 0, z: 0 });
}
};
// Reset experiment
const resetExperiment = () => {
setIsRecording(false);
setRecordedData([]);
dataHistory.current = [];
setMaxValues({ x: 0, y: 0, z: 0 });
};
// Start experiment
const startExperiment = (experiment) => {
setSelectedExperiment(experiment);
setCurrentScreen('experiment');
};
// Render functions
const renderHomeScreen = () => (
<ScrollView style={styles.container}>
<Text style={styles.title}>Science Explorer</Text>
<Text style={styles.subtitle}>Discover science with your device sensors!</Text>
{Object.entries(SCIENCE_CATEGORIES).map(([key, category]) => (
<View key={key} style={styles.categoryContainer}>
<View style={[styles.categoryHeader, { backgroundColor: category.color }]}>
<Text style={styles.categoryEmoji}>{category.emoji}</Text>
<Text style={styles.categoryName}>{category.name}</Text>
</View>
<View style={styles.experimentsGrid}>
{category.experiments.map(experiment => (
<TouchableOpacity
key={experiment.id}
style={styles.experimentButton}
onPress={() => startExperiment(experiment)}
>
<Text style={styles.experimentName}>{experiment.name}</Text>
<Text style={styles.sensorLabel}>
Sensor: {experiment.sensor}
</Text>
</TouchableOpacity>
))}
</View>
</View>
))}
</ScrollView>
);
const renderMotionDetector = () => {
const { x, y, z } = accelerometerData;
const magnitude = Math.sqrt(x ** 2 + y ** 2 + z ** 2);
return (
<View style={styles.experimentContainer}>
<Text style={styles.experimentTitle}>Motion Detector</Text>
{/* Motion Indicator */}
<View style={[
styles.motionIndicator,
motionDetected && styles.motionDetectorActive
]}>
<Text style={styles.motionText}>
{motionDetected ? '🚨 MOTION DETECTED!' : '🟢 No Motion'}
</Text>
</View>
{/* Real-time readings */}
<View style={styles.readingsContainer}>
<View style={styles.readingItem}>
<Text style={styles.readingLabel}>X-Axis:</Text>
<Text style={[styles.readingValue, { color: '#e74c3c' }]}>
{x.toFixed(2)} m/s²
</Text>
</View>
<View style={styles.readingItem}>
<Text style={styles.readingLabel}>Y-Axis:</Text>
<Text style={[styles.readingValue, { color: '#27ae60' }]}>
{y.toFixed(2)} m/s²
</Text>
</View>
<View style={styles.readingItem}>
<Text style={styles.readingLabel}>Z-Axis:</Text>
<Text style={[styles.readingValue, { color: '#3498db' }]}>
{z.toFixed(2)} m/s²
</Text>
</View>
<View style={styles.readingItem}>
<Text style={styles.readingLabel}>Magnitude:</Text>
<Text style={[styles.readingValue, { color: '#9b59b6' }]}>
{magnitude.toFixed(2)} m/s²
</Text>
</View>
</View>
{/* Max Values */}
<View style={styles.maxValuesContainer}>
<Text style={styles.sectionTitle}>Peak Values:</Text>
<Text style={styles.maxValueText}>X: {maxValues.x.toFixed(2)} m/s²</Text>
<Text style={styles.maxValueText}>Y: {maxValues.y.toFixed(2)} m/s²</Text>
<Text style={styles.maxValueText}>Z: {maxValues.z.toFixed(2)} m/s²</Text>
</View>
{/* Controls */}
<View style={styles.controlsContainer}>
<TouchableOpacity
style={[
styles.controlButton,
isRecording ? styles.stopButton : styles.recordButton
]}
onPress={toggleRecording}
>
<Text style={styles.controlButtonText}>
{isRecording ? '⏹ Stop Recording' : '⏺ Record Data'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.resetButton}
onPress={resetExperiment}
>
<Text style={styles.controlButtonText}>🔄 Reset</Text>
</TouchableOpacity>
</View>
{/* Recording indicator */}
{isRecording && (
<View style={styles.recordingIndicator}>
<Text style={styles.recordingText}>
● Recording... ({dataHistory.current.length} points)
</Text>
</View>
)}
</View>
);
};
const renderDataVisualization = () => {
if (recordedData.length === 0) return null;
const chartWidth = width - 40;
const chartHeight = 200;
const maxValue = Math.max(...recordedData.map(d => Math.abs(d.x)),
...recordedData.map(d => Math.abs(d.y)),
...recordedData.map(d => Math.abs(d.z)));
const getY = (value) => chartHeight - ((value / maxValue) * chartHeight / 2) - chartHeight / 2;
const getX = (index) => (index / recordedData.length) * chartWidth;
const createPath = (dataPoints, accessor) => {
return dataPoints.map((point, index) => {
const x = getX(index);
const y = getY(point[accessor]);
return index === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
}).join(' ');
};
return (
<View style={styles.chartContainer}>
<Text style={styles.sectionTitle}>Recorded Data Visualization</Text>
<Svg width={chartWidth} height={chartHeight} style={styles.chart}>
{/* Grid lines */}
<Line x1="0" y1={chartHeight / 2} x2={chartWidth} y2={chartHeight / 2}
stroke="#bdc3c7" strokeWidth="1" />
{/* Data lines */}
<Path d={createPath(recordedData, 'x')} stroke="#e74c3c" strokeWidth="2" fill="none" />
<Path d={createPath(recordedData, 'y')} stroke="#27ae60" strokeWidth="2" fill="none" />
<Path d={createPath(recordedData, 'z')} stroke="#3498db" strokeWidth="2" fill="none" />
</Svg>
<View style={styles.legend}>
<View style={styles.legendItem}>
<View style={[styles.legendColor, { backgroundColor: '#e74c3c' }]} />
<Text style={styles.legendText}>X-Axis</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendColor, { backgroundColor: '#27ae60' }]} />
<Text style={styles.legendText}>Y-Axis</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendColor, { backgroundColor: '#3498db' }]} />
<Text style={styles.legendText}>Z-Axis</Text>
</View>
</View>
</View>
);
};
const renderExperimentScreen = () => {
if (!selectedExperiment) return null;
return (
<ScrollView style={styles.container}>
{/* Header */}
<View style={styles.experimentHeader}>
<TouchableOpacity
style={styles.backButton}
onPress={() => {
setCurrentScreen('home');
setSelectedExperiment(null);
resetExperiment();
}}
>
<Text style={styles.backButtonText}>← Back to Labs</Text>
</TouchableOpacity>
</View>
{/* Experiment content based on type */}
{selectedExperiment.id === 'motion' && renderMotionDetector()}
{selectedExperiment.id === 'gravity' && renderGravityMeter()}
{/* Data visualization */}
{renderDataVisualization()}
</ScrollView>
);
};
return (
<View style={styles.container}>
{currentScreen === 'home' && renderHomeScreen()}
{currentScreen === 'experiment' && renderExperimentScreen()}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
paddingTop: 50,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 10,
},
subtitle: {
fontSize: 18,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 30,
},
categoryContainer: {
marginHorizontal: 20,
marginVertical: 10,
borderRadius: 15,
backgroundColor: 'white',
elevation: 3,
overflow: 'hidden',
},
categoryHeader: {
flexDirection: 'row',
alignItems: 'center',
padding: 15,
},
categoryEmoji: {
fontSize: 24,
marginRight: 10,
},
categoryName: {
fontSize: 20,
fontWeight: 'bold',
color: 'white',
},
experimentsGrid: {
padding: 15,
},
experimentButton: {
backgroundColor: '#ecf0f1',
padding: 15,
borderRadius: 10,
marginVertical: 5,
},
experimentName: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 5,
},
sensorLabel: {
fontSize: 14,
color: '#7f8c8d',
},
experimentHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 10,
},
backButton: {
padding: 10,
},
backButtonText: {
fontSize: 16,
color: '#3498db',
fontWeight: 'bold',
},
experimentContainer: {
padding: 20,
},
experimentTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 20,
},
motionIndicator: {
backgroundColor: '#2ecc71',
borderRadius: 50,
padding: 20,
alignItems: 'center',
marginBottom: 30,
},
motionDetectorActive: {
backgroundColor: '#e74c3c',
transform: [{ scale: 1.1 }],
},
motionText: {
fontSize: 18,
fontWeight: 'bold',
color: 'white',
},
readingsContainer: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
marginBottom: 20,
elevation: 2,
},
readingItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#ecf0f1',
},
readingLabel: {
fontSize: 16,
color: '#34495e',
},
readingValue: {
fontSize: 18,
fontWeight: 'bold',
},
maxValuesContainer: {
backgroundColor: '#ecf0f1',
borderRadius: 10,
padding: 15,
marginBottom: 20,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 10,
},
maxValueText: {
fontSize: 14,
color: '#7f8c8d',
marginBottom: 2,
},
controlsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 20,
},
controlButton: {
paddingHorizontal: 20,
paddingVertical: 15,
borderRadius: 25,
minWidth: 120,
alignItems: 'center',
},
recordButton: {
backgroundColor: '#e74c3c',
},
stopButton: {
backgroundColor: '#95a5a6',
},
resetButton: {
backgroundColor: '#3498db',
},
controlButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
recordingIndicator: {
backgroundColor: '#e74c3c',
borderRadius: 20,
padding: 10,
alignItems: 'center',
},
recordingText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
chartContainer: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
margin: 20,
elevation: 2,
},
chart: {
marginVertical: 10,
},
legend: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 10,
},
legendItem: {
flexDirection: 'row',
alignItems: 'center',
},
legendColor: {
width: 12,
height: 12,
marginRight: 5,
},
legendText: {
fontSize: 12,
color: '#7f8c8d',
},
});
Add more science experiments:
// Gravity meter experiment
const renderGravityMeter = () => {
const gravity = Math.sqrt(
accelerometerData.x ** 2 +
accelerometerData.y ** 2 +
accelerometerData.z ** 2
);
const earthGravity = 9.81; // m/s²
const gravityDifference = Math.abs(gravity - earthGravity);
return (
<View style={styles.experimentContainer}>
<Text style={styles.experimentTitle}>Gravity Meter</Text>
{/* Gravity Display */}
<View style={styles.gravityDisplay}>
<Text style={styles.gravityValue}>{gravity.toFixed(3)}</Text>
<Text style={styles.gravityUnit}>m/s²</Text>
</View>
{/* Earth Gravity Comparison */}
<View style={styles.comparisonContainer}>
<Text style={styles.comparisonTitle}>Comparison to Earth Gravity:</Text>
<Text style={styles.earthGravity}>Earth: {earthGravity} m/s²</Text>
<Text style={[styles.difference, {
color: gravityDifference < 0.5 ? '#27ae60' : '#e74c3c'
}]}>
Difference: {gravityDifference.toFixed(3)} m/s²
</Text>
</View>
{/* Instructions */}
<View style={styles.instructionsContainer}>
<Text style={styles.instructionsTitle}>Instructions:</Text>
<Text style={styles.instructionText}>
• Hold your device still to measure local gravity
</Text>
<Text style={styles.instructionText}>
• Try different orientations (portrait/landscape)
</Text>
<Text style={styles.instructionText}>
• Compare readings at different locations
</Text>
<Text style={styles.instructionText}>
• Values close to 9.81 m/s² indicate accurate measurement
</Text>
</View>
</View>
);
};
// Compass experiment
const renderCompass = () => {
const { x, y, z } = magnetometerData;
const heading = Math.atan2(y, x) * (180 / Math.PI);
const normalizedHeading = (heading + 360) % 360;
const getDirection = (heading) => {
if (heading >= 337.5 || heading < 22.5) return 'N';
if (heading >= 22.5 && heading < 67.5) return 'NE';
if (heading >= 67.5 && heading < 112.5) return 'E';
if (heading >= 112.5 && heading < 157.5) return 'SE';
if (heading >= 157.5 && heading < 202.5) return 'S';
if (heading >= 202.5 && heading < 247.5) return 'SW';
if (heading >= 247.5 && heading < 292.5) return 'W';
return 'NW';
};
return (
<View style={styles.experimentContainer}>
<Text style={styles.experimentTitle}>Digital Compass</Text>
{/* Compass Display */}
<View style={styles.compassContainer}>
<Svg width={200} height={200} style={styles.compass}>
{/* Compass Circle */}
<Circle cx="100" cy="100" r="90" stroke="#34495e" strokeWidth="3" fill="#ecf0f1" />
{/* Cardinal Directions */}
<SvgText x="100" y="20" textAnchor="middle" fontSize="16" fill="#e74c3c" fontWeight="bold">N</SvgText>
<SvgText x="180" y="105" textAnchor="middle" fontSize="16" fill="#34495e">E</SvgText>
<SvgText x="100" y="185" textAnchor="middle" fontSize="16" fill="#34495e">S</SvgText>
<SvgText x="20" y="105" textAnchor="middle" fontSize="16" fill="#34495e">W</SvgText>
{/* Needle */}
<Line
x1="100"
y1="100"
x2={100 + 60 * Math.cos((normalizedHeading - 90) * Math.PI / 180)}
y2={100 + 60 * Math.sin((normalizedHeading - 90) * Math.PI / 180)}
stroke="#e74c3c"
strokeWidth="4"
strokeLinecap="round"
/>
{/* Center dot */}
<Circle cx="100" cy="100" r="5" fill="#2c3e50" />
</Svg>
<View style={styles.headingDisplay}>
<Text style={styles.headingValue}>{normalizedHeading.toFixed(1)}°</Text>
<Text style={styles.direction}>{getDirection(normalizedHeading)}</Text>
</View>
</View>
{/* Magnetic Field Strength */}
<View style={styles.magneticFieldContainer}>
<Text style={styles.fieldTitle}>Magnetic Field Strength:</Text>
<Text style={styles.fieldValue}>X: {x.toFixed(1)} μT</Text>
<Text style={styles.fieldValue}>Y: {y.toFixed(1)} μT</Text>
<Text style={styles.fieldValue}>Z: {z.toFixed(1)} μT</Text>
<Text style={styles.fieldTotal}>
Total: {Math.sqrt(x**2 + y**2 + z**2).toFixed(1)} μT
</Text>
</View>
</View>
);
};
// Add compass-specific styles
gravityDisplay: {
alignItems: 'center',
backgroundColor: '#3498db',
borderRadius: 100,
width: 200,
height: 200,
justifyContent: 'center',
alignSelf: 'center',
marginBottom: 30,
},
gravityValue: {
fontSize: 36,
fontWeight: 'bold',
color: 'white',
},
gravityUnit: {
fontSize: 18,
color: 'white',
marginTop: 5,
},
comparisonContainer: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
marginBottom: 20,
elevation: 2,
},
comparisonTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 10,
},
earthGravity: {
fontSize: 14,
color: '#7f8c8d',
marginBottom: 5,
},
difference: {
fontSize: 16,
fontWeight: 'bold',
},
instructionsContainer: {
backgroundColor: '#ecf0f1',
borderRadius: 10,
padding: 15,
},
instructionsTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 10,
},
instructionText: {
fontSize: 14,
color: '#34495e',
marginBottom: 5,
},
compassContainer: {
alignItems: 'center',
marginBottom: 30,
},
compass: {
marginBottom: 20,
},
headingDisplay: {
alignItems: 'center',
},
headingValue: {
fontSize: 32,
fontWeight: 'bold',
color: '#2c3e50',
},
direction: {
fontSize: 24,
fontWeight: 'bold',
color: '#e74c3c',
marginTop: 5,
},
magneticFieldContainer: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
elevation: 2,
},
fieldTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 10,
},
fieldValue: {
fontSize: 14,
color: '#7f8c8d',
marginBottom: 3,
},
fieldTotal: {
fontSize: 16,
fontWeight: 'bold',
color: '#8e44ad',
marginTop: 5,
},
Create interactive chemistry simulations:
// Virtual chemistry experiments
const [pHLevel, setPHLevel] = useState(7.0);
const [selectedSolution, setSelectedSolution] = useState(null);
const SOLUTIONS = {
water: { name: 'Pure Water', pH: 7.0, color: '#3498db' },
lemon: { name: 'Lemon Juice', pH: 2.0, color: '#f1c40f' },
coffee: { name: 'Coffee', pH: 5.0, color: '#8b4513' },
soap: { name: 'Soap Solution', pH: 9.0, color: '#9b59b6' },
bleach: { name: 'Bleach', pH: 12.0, color: '#ecf0f1' },
};
const renderChemistryLab = () => {
const getPHColor = (pH) => {
if (pH < 7) {
// Acidic - Red spectrum
const intensity = (7 - pH) / 7;
return `rgb(${Math.floor(255 * intensity)}, 0, 0)`;
} else if (pH > 7) {
// Basic - Blue spectrum
const intensity = (pH - 7) / 7;
return `rgb(0, 0, ${Math.floor(255 * intensity)})`;
}
return '#2ecc71'; // Neutral - Green
};
const getPHDescription = (pH) => {
if (pH < 3) return 'Strongly Acidic';
if (pH < 7) return 'Acidic';
if (pH === 7) return 'Neutral';
if (pH < 11) return 'Basic';
return 'Strongly Basic';
};
return (
<View style={styles.experimentContainer}>
<Text style={styles.experimentTitle}>pH Simulator</Text>
{/* pH Display */}
<View style={styles.pHMeter}>
<View style={[styles.pHSolution, { backgroundColor: getPHColor(pHLevel) }]}>
<Text style={styles.pHValue}>{pHLevel.toFixed(1)}</Text>
<Text style={styles.pHLabel}>pH</Text>
</View>
<Text style={styles.pHDescription}>{getPHDescription(pHLevel)}</Text>
</View>
{/* pH Scale */}
<View style={styles.pHScale}>
<Text style={styles.scaleTitle}>pH Scale</Text>
<View style={styles.scaleBar}>
{Array.from({length: 15}, (_, i) => (
<View
key={i}
style={[
styles.scaleSegment,
{ backgroundColor: getPHColor(i) },
pHLevel >= i && pHLevel < i + 1 && styles.activeSegment
]}
>
<Text style={styles.scaleNumber}>{i}</Text>
</View>
))}
</View>
</View>
{/* Solution Selector */}
<View style={styles.solutionsContainer}>
<Text style={styles.sectionTitle}>Test Solutions:</Text>
<View style={styles.solutionGrid}>
{Object.entries(SOLUTIONS).map(([key, solution]) => (
<TouchableOpacity
key={key}
style={[
styles.solutionButton,
{ backgroundColor: solution.color },
selectedSolution === key && styles.selectedSolution
]}
onPress={() => {
setSelectedSolution(key);
setPHLevel(solution.pH);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Text style={styles.solutionName}>{solution.name}</Text>
<Text style={styles.solutionPH}>pH {solution.pH}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Custom pH Slider */}
<View style={styles.customPHContainer}>
<Text style={styles.sectionTitle}>Custom pH:</Text>
<View style={styles.sliderContainer}>
<Text style={styles.sliderLabel}>0</Text>
<View style={styles.slider}>
<View
style={[
styles.sliderThumb,
{ left: `${(pHLevel / 14) * 100}%` },
{ backgroundColor: getPHColor(pHLevel) }
]}
/>
</View>
<Text style={styles.sliderLabel}>14</Text>
</View>
</View>
</View>
);
};
// Chemistry lab styles
pHMeter: {
alignItems: 'center',
marginBottom: 30,
},
pHSolution: {
width: 150,
height: 150,
borderRadius: 75,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10,
},
pHValue: {
fontSize: 32,
fontWeight: 'bold',
color: 'white',
},
pHLabel: {
fontSize: 18,
color: 'white',
},
pHDescription: {
fontSize: 18,
fontWeight: 'bold',
color: '#2c3e50',
},
pHScale: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
marginBottom: 20,
elevation: 2,
},
scaleTitle: {
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 10,
color: '#2c3e50',
},
scaleBar: {
flexDirection: 'row',
height: 40,
},
scaleSegment: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: '#bdc3c7',
},
activeSegment: {
borderWidth: 3,
borderColor: '#2c3e50',
},
scaleNumber: {
fontSize: 12,
color: 'white',
fontWeight: 'bold',
},
solutionsContainer: {
backgroundColor: '#ecf0f1',
borderRadius: 15,
padding: 20,
marginBottom: 20,
},
solutionGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
solutionButton: {
width: '45%',
padding: 15,
borderRadius: 10,
alignItems: 'center',
marginVertical: 5,
},
selectedSolution: {
borderWidth: 3,
borderColor: '#2c3e50',
},
solutionName: {
fontSize: 14,
fontWeight: 'bold',
color: 'white',
textAlign: 'center',
},
solutionPH: {
fontSize: 12,
color: 'white',
marginTop: 2,
},
customPHContainer: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
elevation: 2,
},
sliderContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
sliderLabel: {
fontSize: 14,
color: '#7f8c8d',
marginHorizontal: 10,
},
slider: {
flex: 1,
height: 6,
backgroundColor: '#bdc3c7',
borderRadius: 3,
position: 'relative',
},
sliderThumb: {
position: 'absolute',
width: 20,
height: 20,
borderRadius: 10,
top: -7,
marginLeft: -10,
borderWidth: 2,
borderColor: 'white',
},
Science Exploration App Successfully Built If:
Time Investment: 60 minutes total Difficulty Level: Advanced Prerequisites: React Native basics, understanding of basic science and sensor concepts Tools Needed: Physical device with sensors, basic understanding of physics and chemistry