JavaScript Browser APIs
Introduction to Browser APIs
Modern web browsers provide a rich set of Application Programming Interfaces (APIs) that allow JavaScript code to interact with the browser environment, the operating system, and various hardware features. These APIs extend the capabilities of JavaScript beyond its core language features, enabling developers to build powerful and interactive web applications.
Understanding and utilizing these APIs effectively is crucial for creating dynamic user experiences, managing data, handling network requests, and optimizing performance.
- Document Object Model (DOM) Manipulation
- Network Requests (Fetch, XMLHttpRequest)
- Client-Side Storage (Web Storage, IndexedDB)
- User Location (Geolocation)
- Graphics and Multimedia (Canvas, WebGL, Web Audio)
- Asynchronous Operations and Background Tasks (Web Workers, Service Workers)
- Real-time Communication (WebSockets)
- User Interface Interactions (Notifications, History API)
Core Browser APIs Overview
1. Document Object Model (DOM) API
The DOM represents the structure of an HTML or XML document as a tree of objects (nodes). The DOM API provides the interface for JavaScript to dynamically access and manipulate the content, structure, and style of these documents.
Understanding the DOM Tree
Every HTML element, attribute, and piece of text is represented as a node in the DOM tree. The document
object is the root node.
- Element Nodes: Represent HTML tags (e.g.,
<div>
,<p>
,<a>
). - Text Nodes: Represent the text content within elements.
- Attribute Nodes: Represent element attributes (e.g.,
id
,class
,href
). - Comment Nodes: Represent HTML comments.
Selecting Elements (Finding Nodes)
Various methods allow you to find specific nodes in the DOM.
// --- Specific Element Selection ---
// Select by ID (fastest, returns single element or null)
const mainTitle = document.getElementById('main-title');
console.log('Element by ID:', mainTitle);
// --- Multiple Element Selection ---
// Select by Class Name (returns live HTMLCollection)
const primaryButtons = document.getElementsByClassName('btn-primary');
console.log('Elements by Class Name (Live HTMLCollection):', primaryButtons);
// Access elements like an array: primaryButtons[0]
// Select by Tag Name (returns live HTMLCollection)
const allParagraphs = document.getElementsByTagName('p');
console.log('Elements by Tag Name (Live HTMLCollection):', allParagraphs);
// --- Modern Selector-Based Selection ---
// Select by CSS Selector (returns first matching element or null)
const firstProduct = document.querySelector('.product-list .product-item:first-child');
console.log('First Matching Element:', firstProduct);
// Select by CSS Selector (returns static NodeList)
const warningAlerts = document.querySelectorAll('div.alert.alert-warning');
console.log('All Matching Elements (Static NodeList):', warningAlerts);
// Iterate using forEach: warningAlerts.forEach(alert => { /* ... */ });
// Selecting within an element
const productList = document.getElementById('product-list');
const itemsInList = productList?.querySelectorAll('.product-item'); // Search only within productList
console.log('Items within specific list:', itemsInList);
Live vs. Static Collections: HTMLCollection
(from getElementsByClassName
/TagName
) updates automatically if the DOM changes. NodeList
(from querySelectorAll
) is a static snapshot and does not update automatically.
Traversing the DOM (Moving Between Nodes)
Once you have selected an element, you can navigate to related elements.
const currentItem = document.getElementById('item-2');
if (currentItem) {
// Get Parent
const parentList = currentItem.parentNode; // Or currentItem.parentElement (often preferred)
console.log('Parent Element:', parentList);
// Get Children (includes text nodes, comments)
const allChildrenNodes = parentList.childNodes;
console.log('All Child Nodes:', allChildrenNodes);
// Get Element Children (only element nodes - usually more useful)
const elementChildren = parentList.children; // HTMLCollection
console.log('Element Children:', elementChildren);
// First and Last Element Child
const firstChild = parentList.firstElementChild;
const lastChild = parentList.lastElementChild;
console.log('First Child:', firstChild, 'Last Child:', lastChild);
// Get Siblings
const nextSibling = currentItem.nextElementSibling;
const prevSibling = currentItem.previousElementSibling;
console.log('Next Sibling:', nextSibling, 'Previous Sibling:', prevSibling);
}
Manipulating Elements (Changing the DOM)
Create, add, remove, and modify elements and their content.
// --- Creating and Adding Elements ---
// Create a new element
const newItem = document.createElement('li');
newItem.textContent = 'Item 4 (New)'; // Set text content safely
newItem.setAttribute('id', 'item-4');
// Append to the end of a parent
const list = document.getElementById('my-list');
list?.appendChild(newItem);
// Insert before a specific element
const item3 = document.getElementById('item-3');
const anotherNewItem = document.createElement('li');
anotherNewItem.innerHTML = '<strong>Item 2.5</strong> (Inserted)'; // Can insert HTML (use carefully)
list?.insertBefore(anotherNewItem, item3);
// --- Removing and Replacing Elements ---
// Remove an element
const itemToRemove = document.getElementById('item-1');
itemToRemove?.remove(); // Modern and simple
// Or: itemToRemove.parentNode.removeChild(itemToRemove); // Older way
// Replace an element
const itemToReplace = document.getElementById('item-2');
const replacementItem = document.createElement('li');
replacementItem.textContent = 'Item 2 (Replaced)';
itemToReplace?.parentNode?.replaceChild(replacementItem, itemToReplace);
// --- Modifying Content and Attributes ---
const heading = document.getElementById('main-title');
if (heading) {
heading.textContent = 'Updated Main Title'; // Safer for plain text
// heading.innerHTML = 'Updated <em>Main</em> Title'; // Use for HTML, risk of XSS if content is user-generated
// Work with attributes
heading.setAttribute('data-status', 'updated');
const status = heading.getAttribute('data-status');
console.log('Heading status:', status);
heading.removeAttribute('data-old-attr');
console.log('Has class `main`?', heading.hasAttribute('class'));
// Work with classes (recommended over className string manipulation)
heading.classList.add('highlighted', 'important');
heading.classList.remove('old-class');
heading.classList.toggle('active'); // Adds if absent, removes if present
console.log('Heading classes:', heading.classList);
// Work with styles (directly sets inline styles)
heading.style.color = 'purple';
heading.style.fontSize = '2.5rem'; // Use camelCase for CSS properties with hyphens
heading.style.backgroundColor = '#f0f0f0';
// Remove a style
// heading.style.backgroundColor = '';
}
Event Handling (Responding to Interactions)
The DOM allows you to listen for and react to events triggered by user actions (clicks, keypresses, mouse movements) or browser actions (page load, resize).
Adding Event Listeners
const actionButton = document.getElementById('action-button');
function onButtonClick(event) {
console.log('--- Button Click Event ---');
console.log('Event Type:', event.type); // e.g., 'click'
console.log('Target Element:', event.target); // The element that triggered the event (the button)
console.log('Current Target:', event.currentTarget); // The element the listener is attached to (usually same as target here)
console.log('Timestamp:', event.timeStamp);
console.log('Mouse Coordinates (relative to window):', event.clientX, event.clientY);
// Prevent default action (e.g., form submission, link navigation)
// event.preventDefault();
// Stop the event from bubbling up to parent elements
// event.stopPropagation();
event.target.textContent = 'Clicked!';
event.target.disabled = true;
}
if (actionButton) {
// Recommended: addEventListener(eventType, handlerFunction, options/useCapture)
actionButton.addEventListener('click', onButtonClick);
// Add another listener for mouse over
actionButton.addEventListener('mouseover', () => {
actionButton.style.backgroundColor = 'lightblue';
});
actionButton.addEventListener('mouseout', () => {
actionButton.style.backgroundColor = ''; // Reset style
});
}
// Removing Event Listeners (requires the exact same function reference)
function removeListenerExample() {
if (actionButton) {
actionButton.removeEventListener('click', onButtonClick);
console.log('Click listener removed.');
}
}
// setTimeout(removeListenerExample, 5000); // Example: remove after 5 seconds
Event Propagation (Bubbling and Capturing)
Events typically travel in two phases:
- Capturing Phase: The event travels down from the window to the target element. Listeners attached with
useCapture: true
(or the third argument astrue
) fire during this phase. - Bubbling Phase: The event travels back up from the target element to the window. This is the default phase for listeners (
useCapture: false
).
event.stopPropagation()
prevents the event from traveling further (up or down depending on the phase).
Event Delegation
Instead of adding listeners to many individual child elements, add a single listener to a common parent. Check event.target
inside the handler to determine which child was interacted with. This is more efficient, especially for dynamically added elements.
const userList = document.getElementById('user-list');
userList?.addEventListener('click', function(event) {
// Check if the clicked element is a button inside a list item
if (event.target && event.target.matches('li button.delete-btn')) {
const listItem = event.target.closest('li'); // Find the parent
const userName = listItem?.dataset.userName || 'Unknown User';
console.log(`Delete button clicked for user: ${userName}`);
// listItem?.remove(); // Example: Remove the list item
}
});
2. Fetch API
A modern, promise-based interface for making network requests (HTTP and others). It supersedes the older XMLHttpRequest
object, offering a cleaner and more powerful API.
Basic GET Request & Response Object
The fetch()
function initiates a request and returns a promise that resolves to the Response
object representing the server's response.
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => {
console.log('--- Response Object ---');
console.log('Status Code:', response.status); // e.g., 200, 404, 500
console.log('Status Text:', response.statusText); // e.g., "OK", "Not Found"
console.log('OK Status (200-299):', response.ok); // boolean
console.log('Response Type:', response.type); // e.g., 'basic', 'cors'
console.log('Response URL:', response.url);
// Accessing Headers
console.log('Content-Type Header:', response.headers.get('Content-Type'));
// Iterate over headers
// for (let [key, value] of response.headers) {
// console.log(`${key}: ${value}`);
// }
// IMPORTANT: Check response.ok before processing the body!
if (!response.ok) {
// Handle HTTP errors (e.g., 404 Not Found, 500 Internal Server Error)
throw new Error(`HTTP error! Status: ${response.status}`);
}
// The response body is a ReadableStream. Use methods to consume it:
return response.json(); // Consumes the body and parses as JSON
// return response.text(); // Consumes the body as plain text
// return response.blob(); // Consumes the body as a Blob (binary data)
// return response.formData(); // Consumes the body as FormData
// return response.arrayBuffer(); // Consumes the body as an ArrayBuffer
})
.then(data => {
console.log('Processed Body Data (JSON):', data);
// Use the data (e.g., display title: data.title)
})
.catch(error => {
// Handles network errors (e.g., DNS resolution failure, server unreachable)
// AND errors thrown manually (like the HTTP status check above)
console.error('Fetch Operation Failed:', error);
});
Important: The promise returned by fetch()
only rejects on network errors, not on HTTP error statuses (like 4xx or 5xx). You **must** check response.ok
or response.status
manually.
Making POST, PUT, DELETE Requests
Use the second argument of fetch()
, an options object, to configure the request method, headers, and body.
POST Request (Sending JSON)
const newPost = {
title: 'My New Post',
body: 'This is the content of the post.',
userId: 5,
};
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
// Tells the server we are sending JSON
'Content-Type': 'application/json; charset=UTF-8',
// Example Authorization header
// 'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
},
// Convert the JavaScript object to a JSON string for the body
body: JSON.stringify(newPost),
})
.then(response => {
if (!response.ok) {
// Maybe read error details from response body if server sends them
// return response.json().then(errData => { throw new Error(...) });
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Often, POST responses return the created object with an ID
})
.then(createdPost => {
console.log('POST Success:', createdPost);
})
.catch(error => {
console.error('POST Error:', error);
});
POST Request (Sending FormData)
Useful for sending form data, including file uploads. The browser automatically sets the correct Content-Type
(multipart/form-data
).
const formData = new FormData();
formData.append('username', 'testuser');
formData.append('email', 'test@example.com');
// Example: Append a file from an input element
// const fileInput = document.getElementById('file-input');
// if (fileInput.files.length > 0) {
// formData.append('profilePic', fileInput.files[0], 'avatar.jpg');
// }
fetch('/api/users', {
method: 'POST',
// NO 'Content-Type' header needed here - browser sets it with boundary
body: formData,
})
.then(response => response.json())
.then(result => console.log('FormData POST Success:', result))
.catch(error => console.error('FormData POST Error:', error));
PUT Request (Updating Data)
const updatedData = { title: 'Updated Title', body: 'Updated content.' };
fetch('https://jsonplaceholder.typicode.com/posts/1', { // Specify the ID of the resource to update
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedData),
})
.then(response => response.json())
.then(data => console.log('PUT Success:', data))
.catch(error => console.error('PUT Error:', error));
DELETE Request
fetch('https://jsonplaceholder.typicode.com/posts/1', { // Specify the ID to delete
method: 'DELETE',
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// DELETE often returns no body or an empty body, status code 200 or 204
console.log('DELETE Success, Status:', response.status);
// return response.json(); // Only if the server sends a JSON confirmation
})
.catch(error => {
console.error('DELETE Error:', error);
});
Using Request and Headers Objects
For more complex scenarios or reusability, you can create explicit Request
and Headers
objects.
// Create Headers
const myHeaders = new Headers();
myHeaders.append('Content-Type', 'application/json');
myHeaders.append('X-Custom-Header', 'MyValue');
// Create Request object
const requestOptions = {
method: 'GET',
headers: myHeaders,
mode: 'cors', // Handling Cross-Origin Requests
cache: 'no-cache', // Control caching behavior
// Other options: redirect, referrer, integrity, etc.
};
const myRequest = new Request('https://jsonplaceholder.typicode.com/users/2', requestOptions);
// Use the Request object with fetch
fetch(myRequest)
.then(response => response.json())
.then(data => console.log('Fetched using Request object:', data))
.catch(error => console.error('Error with Request object:', error));
Aborting Fetch Requests
Use the AbortController
API to cancel an ongoing fetch request, useful for scenarios like user cancelling an action or implementing timeouts.
// Create an AbortController
const controller = new AbortController();
const signal = controller.signal; // Get the signal object
// Set a timeout to abort the request after 3 seconds
const timeoutId = setTimeout(() => {
console.log('Aborting fetch due to timeout...');
controller.abort();
}, 3000);
console.log('Starting fetch with abort signal...');
fetch('https://httpbin.org/delay/5', { // This endpoint delays response for 5 seconds
signal: signal // Pass the signal to the fetch options
})
.then(response => {
clearTimeout(timeoutId); // Clear the timeout if fetch completes in time
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.text();
})
.then(data => {
console.log('Fetch completed successfully (within timeout):', data);
})
.catch(error => {
clearTimeout(timeoutId); // Clear timeout in case of other errors
if (error.name === 'AbortError') {
console.error('Fetch aborted:', error.message);
} else {
console.error('Fetch error (not abort):', error);
}
});
// To manually abort (e.g., on a button click):
// document.getElementById('cancelBtn').addEventListener('click', () => controller.abort());
Using Async/Await
Async/await provides a more synchronous-looking way to handle the promises involved in fetching data.
async function fetchUserData(userId) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const userData = await response.json();
console.log('User data (async/await):', userData);
return userData;
} catch (error) {
console.error('Failed to fetch user data:', error);
// Optionally re-throw or return a specific error state
return null;
}
}
// Call the async function
fetchUserData(3);
fetchUserData(999); // Example of handling a 404 error
3. Web Storage API
Provides simple mechanisms for web pages to store key-value data locally within the user's browser, persisting across page loads. It consists of two main objects: localStorage
and sessionStorage
.
localStorage
vs. sessionStorage
Feature | localStorage |
sessionStorage |
---|---|---|
Persistence | Persists until explicitly deleted (by user or code). Data remains after browser closes and reopens. | Persists only for the duration of the page session (until the tab/window is closed). |
Scope | Scoped to the document's origin (protocol + host + port). Accessible from any script from the same origin. | Scoped to the document's origin AND the specific browser tab/window instance. Data in one tab is not accessible from another tab, even with the same origin. |
Storage Limit | Typically 5-10MB per origin (browser-dependent). | Typically 5-10MB per origin (browser-dependent). |
Availability | Available in all windows/tabs from the same origin. | Isolated to the specific tab/window that created it. |
Core Methods (Common to both)
Both localStorage
and sessionStorage
share the same API methods.
// Using localStorage for examples (same methods apply to sessionStorage)
// 1. setItem(key, value): Store a key-value pair. Both must be strings.
localStorage.setItem('username', 'Alice');
localStorage.setItem('theme', 'dark');
localStorage.setItem('loginTimestamp', new Date().toISOString());
// 2. getItem(key): Retrieve the value associated with a key (returns string or null).
const username = localStorage.getItem('username');
console.log('Retrieved username:', username); // Output: Alice
const nonExistent = localStorage.getItem('nonExistentKey');
console.log('Non-existent key:', nonExistent); // Output: null
// 3. removeItem(key): Remove a specific key-value pair.
localStorage.removeItem('theme');
console.log('Theme after removal:', localStorage.getItem('theme')); // Output: null
// 4. clear(): Remove ALL key-value pairs for the current origin.
// localStorage.clear(); // Use with caution!
// console.log('Username after clear:', localStorage.getItem('username')); // Output: null (if clear() was run)
// 5. length: Get the number of stored items.
console.log('Number of items in localStorage:', localStorage.length);
// 6. key(index): Get the key at a specific numerical index (order is not guaranteed).
if (localStorage.length > 0) {
const firstKey = localStorage.key(0);
console.log('Key at index 0:', firstKey);
console.log('Value for first key:', localStorage.getItem(firstKey));
}
// Alternative Access (like an object - less recommended due to potential conflicts)
// localStorage.username = 'Bob'; // Equivalent to setItem
// console.log(localStorage['username']); // Equivalent to getItem
// delete localStorage.username; // Equivalent to removeItem
Storing Non-String Data (Objects, Arrays)
Web Storage only stores strings. To store complex data like objects or arrays, you must serialize them (e.g., using JSON.stringify
) before storing and deserialize (JSON.parse
) upon retrieval.
const userSettings = {
notifications: true,
fontSize: 14,
tags: ['javascript', 'webdev']
};
// Store the object
try {
localStorage.setItem('userSettings', JSON.stringify(userSettings));
console.log('Stored user settings object.');
} catch (e) {
console.error('Error stringifying user settings:', e);
}
// Retrieve and parse the object
let retrievedSettings = null;
const settingsString = localStorage.getItem('userSettings');
if (settingsString) {
try {
retrievedSettings = JSON.parse(settingsString);
console.log('Retrieved user settings:', retrievedSettings);
console.log('Retrieved font size:', retrievedSettings.fontSize); // Access properties
} catch (e) {
console.error('Error parsing stored settings:', e);
// Handle potential corruption of stored data
}
}
Use Cases
- User Preferences: Storing theme choices, font sizes, layout settings (
localStorage
). - Offline Data Cache: Caching non-critical application data retrieved from the server (
localStorage
). - Session State: Keeping track of temporary states within a single user session, like items in a shopping cart before login (
sessionStorage
). - Drafts: Saving user input in forms temporarily in case of accidental navigation (
sessionStorage
orlocalStorage
). - Feature Flags: Storing simple flags to enable/disable features for testing (
localStorage
).
Performance and Security Considerations
- Synchronous Operations: All Web Storage operations (
setItem
,getItem
, etc.) are synchronous. Heavy usage, especially within loops, can block the main thread and impact UI responsiveness. Use judiciously. - Limited Size: Typically 5-10MB per origin. Not suitable for very large datasets. Check for potential errors if the quota is exceeded (usually throws a
QuotaExceededError
). - String-Only: Requires serialization/deserialization for complex data.
- Security Risk (XSS): Data stored in Web Storage is accessible via JavaScript from the same origin. If your site is vulnerable to Cross-Site Scripting (XSS), malicious scripts could steal or modify this data. Do NOT store sensitive information (like passwords, session tokens, personal data) in Web Storage. Use secure, HTTP-only cookies or server-side sessions for sensitive data.
The `storage` Event
Browsers fire a storage
event on the window
object in other open tabs/windows from the same origin whenever localStorage
is modified (added, updated, or removed). It does *not* fire in the same tab that made the change.
window.addEventListener('storage', (event) => {
console.log('--- Storage Event Detected ---');
console.log('Key changed:', event.key); // Key that was changed (null if clear() was called)
console.log('Old value:', event.oldValue); // Previous value (null if new item or clear())
console.log('New value:', event.newValue); // New value (null if removed or clear())
console.log('URL of page that made change:', event.url);
console.log('Storage Area:', event.storageArea); // The localStorage object itself
// Example: Reload user settings if they changed in another tab
if (event.key === 'userSettings') {
const newSettings = event.newValue ? JSON.parse(event.newValue) : null;
console.log('User settings updated in another tab. Applying changes...');
// applyUserSettings(newSettings);
}
});
// To test: Open two tabs from the same origin.
// In Tab 1, execute: localStorage.setItem('testKey', 'hello ' + Date.now());
// Observe the console logs in Tab 2.
Note: The storage
event is not fired for sessionStorage
changes.
4. Geolocation API
Allows web applications to access the user's geographical location, typically with their permission. Access is provided through the navigator.geolocation
object.
Checking for Support
Before using the API, it's good practice to check if the browser supports it.
if ('geolocation' in navigator) {
console.log('Geolocation API is available.');
// Proceed to use the API
} else {
console.error('Geolocation API is not supported by this browser.');
// Provide fallback or inform the user
}
getCurrentPosition()
- Getting a Single Location Fix
Requests the user's current position once. It takes up to three arguments: a success callback, an optional error callback, and an optional options object.
Syntax
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options);
Success Callback
Called when the location is successfully retrieved. It receives a GeolocationPosition
object as an argument.
function successCallback(position) {
console.log('--- Geolocation Position ---');
console.log('Timestamp:', new Date(position.timestamp).toLocaleString());
const coords = position.coords;
console.log('Latitude:', coords.latitude);
console.log('Longitude:', coords.longitude);
console.log('Accuracy (meters):', coords.accuracy);
// Optional properties (may be null)
console.log('Altitude (meters):', coords.altitude);
console.log('Altitude Accuracy (meters):', coords.altitudeAccuracy);
console.log('Heading (degrees from North):', coords.heading);
console.log('Speed (meters/second):', coords.speed);
// Example: Display on a map (requires a mapping library like Leaflet or Google Maps)
// showMap(coords.latitude, coords.longitude);
}
Error Callback
Called when an error occurs while trying to get the location. It receives a GeolocationPositionError
object.
function errorCallback(error) {
console.error('--- Geolocation Error ---');
console.error('Error Code:', error.code);
console.error('Error Message:', error.message);
switch (error.code) {
case error.PERMISSION_DENIED:
console.error("User denied the request for Geolocation.");
// Guide user on how to enable permissions
break;
case error.POSITION_UNAVAILABLE:
console.error("Location information is unavailable.");
// Network issue or location sources disabled
break;
case error.TIMEOUT:
console.error("The request to get user location timed out.");
// Try again or inform user
break;
case error.UNKNOWN_ERROR:
console.error("An unknown error occurred.");
break;
}
}
Error Codes:
1
:PERMISSION_DENIED
- User blocked access.2
:POSITION_UNAVAILABLE
- Internal error, location sources disabled, or network issue.3
:TIMEOUT
- Failed to get location within the specified time.
Options Object
An optional object to fine-tune the location request.
const options = {
enableHighAccuracy: true, // Request the most accurate position possible (can consume more power/time). Default: false.
timeout: 10000, // Maximum time (ms) allowed to get position. Default: Infinity.
maximumAge: 60000 // Maximum age (ms) of a cached position to accept. Default: 0 (always get fresh). Set to Infinity to only use cached.
};
// Putting it all together
if ('geolocation' in navigator) {
console.log('Requesting current position...');
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options);
} else {
console.error('Geolocation API not supported.');
}
watchPosition()
- Tracking Location Changes
Registers a handler function that will be called automatically each time the device's position changes. It also accepts success, error, and options arguments. It returns an ID that can be used to stop watching.
let watchId = null;
function startWatching() {
if ('geolocation' in navigator) {
if (watchId) {
console.log('Already watching position.');
return;
}
console.log('Starting position watch...');
// Same success and error callbacks as getCurrentPosition can be used
// Often uses lower accuracy or longer maximumAge for efficiency
const watchOptions = { enableHighAccuracy: false, timeout: 15000, maximumAge: 30000 };
watchId = navigator.geolocation.watchPosition(handlePositionUpdate, errorCallback, watchOptions);
console.log('Watch ID:', watchId);
} else {
console.error('Geolocation API not supported.');
}
}
function handlePositionUpdate(position) {
console.log('--- Position Update ---');
console.log('Time:', new Date(position.timestamp).toLocaleTimeString());
console.log(`Lat: ${position.coords.latitude.toFixed(4)}, Lon: ${position.coords.longitude.toFixed(4)}`);
// Update map marker, display coordinates, etc.
}
function stopWatching() {
if (watchId !== null && navigator.geolocation) {
console.log('Stopping position watch with ID:', watchId);
navigator.geolocation.clearWatch(watchId);
watchId = null;
} else {
console.log('Not currently watching position.');
}
}
// Example Usage:
// startWatching();
// setTimeout(stopWatching, 60000); // Stop watching after 60 seconds
Permissions, Privacy, and Security
- User Consent is Mandatory: Browsers **must** ask for the user's permission before sharing location data. The request UI varies by browser.
- HTTPS Required: As mentioned, secure contexts are generally required.
- Transparency: Clearly explain to users *why* you need their location and *how* it will be used.
- Least Privilege: Only ask for location when necessary. Don't request high accuracy if low accuracy suffices. Stop watching when the feature is no longer active.
- Data Handling: Handle retrieved location data responsibly. Avoid logging or storing precise locations unless essential and with appropriate security measures.
- Fallback Mechanisms: Provide alternative functionality if the user denies permission or if location services are unavailable.
Use Cases
- Displaying the user's location on a map.
- Providing location-based recommendations (nearby restaurants, events).
- Geotagging photos or posts.
- Turn-by-turn navigation (requires continuous updates with
watchPosition
). - Weather applications showing local forecasts.
- Fitness tracking applications.
5. Canvas API
Provides a powerful way to draw graphics, manipulate images, and create animations directly within an HTML <canvas>
element using JavaScript. It's primarily used for 2D graphics, although WebGL (accessed via canvas) enables 3D rendering.
Setting Up the Canvas
First, add a <canvas>
element to your HTML, giving it an ID and dimensions.
<!-- Add fallback content for browsers that don't support canvas -->
<canvas id="myCanvas" width="600" height="400" style="border: 1px solid black;">
Your browser does not support the HTML canvas element.
</canvas>
Then, get a reference to the element and its 2D rendering context in JavaScript.
const canvas = document.getElementById('myCanvas');
// Check if canvas is supported and get the context
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
console.log('2D context obtained:', ctx);
// All drawing operations happen on the 'ctx' object
// --- Start Drawing Examples Here ---
} else {
console.error('Canvas not supported or context could not be created.');
// Handle lack of support
}
Basic Drawing: Rectangles
Methods for drawing filled, outlined, or clearing rectangular areas.
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
// Set fill color
ctx.fillStyle = 'rgb(200, 0, 0)'; // Red
// Draw a filled rectangle: fillRect(x, y, width, height)
ctx.fillRect(10, 10, 100, 50);
// Set stroke (outline) color
ctx.strokeStyle = 'rgba(0, 0, 200, 0.5)'; // Semi-transparent blue
// Draw a rectangular outline: strokeRect(x, y, width, height)
ctx.strokeRect(120, 10, 100, 50);
// Clear a rectangular area, making it transparent: clearRect(x, y, width, height)
ctx.clearRect(30, 20, 40, 30);
}
Drawing Paths (Lines, Shapes)
Paths are used to create complex shapes, lines, curves, etc.
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
// --- Draw a Triangle ---
ctx.beginPath(); // Start a new path
ctx.moveTo(75, 100); // Move pen to starting point (x, y)
ctx.lineTo(125, 100); // Draw line to (x, y)
ctx.lineTo(100, 150); // Draw line to next point
ctx.closePath(); // Close the path (connects last point to first)
// Style and draw the path
ctx.lineWidth = 3;
ctx.strokeStyle = '#33cc33'; // Green
ctx.stroke(); // Draw the outline
// --- Draw a Filled Shape (Pentagon) ---
ctx.beginPath();
ctx.moveTo(200, 125);
ctx.lineTo(240, 100);
ctx.lineTo(240, 150);
ctx.lineTo(200, 175);
ctx.lineTo(160, 150);
ctx.lineTo(160, 100);
ctx.closePath();
ctx.fillStyle = '#cc33cc'; // Purple
ctx.fill(); // Fill the closed path
}
Drawing Arcs and Circles
The arc()
method creates arcs or circles.
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
// arc(x, y, radius, startAngle, endAngle, anticlockwise?)
// Angles are in radians (Math.PI radians = 180 degrees)
// Full circle
ctx.beginPath();
ctx.arc(300, 125, 40, 0, Math.PI * 2); // Full circle
ctx.fillStyle = 'orange';
ctx.fill();
// Pac-man shape (arc segment)
ctx.beginPath();
ctx.moveTo(400, 125); // Move to center
ctx.arc(400, 125, 30, 0.2 * Math.PI, 1.8 * Math.PI, false); // Draw arc
ctx.closePath(); // Line back to center
ctx.fillStyle = 'yellow';
ctx.fill();
}
Drawing Text
Add text directly to the canvas.
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
ctx.font = 'bold 24px Arial';
ctx.fillStyle = 'navy';
ctx.textAlign = 'center'; // 'left', 'right', 'center', 'start', 'end'
ctx.textBaseline = 'middle'; // 'top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'
// fillText(text, x, y, maxWidth?)
ctx.fillText('Hello Canvas!', canvas.width / 2, 200);
ctx.strokeStyle = 'red';
ctx.lineWidth = 0.5;
// strokeText(text, x, y, maxWidth?)
ctx.strokeText('Outlined Text', canvas.width / 2, 240);
}
Styling
Control the appearance of drawings.
- Colors:
fillStyle
,strokeStyle
(accepts CSS color strings: names, hex, rgb, rgba, hsl, hsla). - Line Styles:
lineWidth
(number),lineCap
('butt', 'round', 'square'),lineJoin
('round', 'bevel', 'miter'),miterLimit
(number),setLineDash([segments])
,lineDashOffset
. - Gradients:
createLinearGradient(x0, y0, x1, y1)
createRadialGradient(x0, y0, r0, x1, y1, r1)
- Gradients need color stops added:
gradient.addColorStop(offset, color)
(offset 0.0 to 1.0). - Assign the gradient object to
fillStyle
orstrokeStyle
.
- Patterns:
createPattern(image, repetition)
(image can be<img>
,<canvas>
,<video>
; repetition: 'repeat', 'repeat-x', 'repeat-y', 'no-repeat'). Assign the pattern object tofillStyle
orstrokeStyle
. - Shadows:
shadowOffsetX
,shadowOffsetY
,shadowBlur
,shadowColor
. - Global Alpha:
globalAlpha
(0.0 to 1.0 for transparency).
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
// Linear Gradient Example
const linGrad = ctx.createLinearGradient(0, 250, 0, 350);
linGrad.addColorStop(0, 'blue');
linGrad.addColorStop(0.5, 'cyan');
linGrad.addColorStop(1, 'white');
ctx.fillStyle = linGrad;
ctx.fillRect(10, 250, 150, 100);
ctx.strokeStyle = 'red';
ctx.lineWidth = 0.5;
// strokeText(text, x, y, maxWidth?)
ctx.strokeText('Outlined Text', canvas.width / 2, 240);
}
Transformations
Modify the coordinate system to move, rotate, or scale drawings without changing the drawing commands themselves.
translate(x, y)
: Moves the origin (0,0 point) of the canvas.rotate(angle)
: Rotates the canvas around the current origin (angle in radians).scale(x, y)
: Scales the drawing units (x for horizontal, y for vertical).transform(a, b, c, d, e, f)
: Applies a custom transformation matrix.setTransform(a, b, c, d, e, f)
: Resets the current transform and applies a new one.resetTransform()
: Resets the transform to the identity matrix (no transformation).
Important: Transformations stack. Use save()
and restore()
to isolate transformations.
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.fillRect(350, 260, 80, 40); // Original rectangle
// Translate, Rotate, and draw again
ctx.translate(390, 280); // Move origin to the center of the rectangle (350 + 80/2, 260 + 40/2)
ctx.rotate(Math.PI / 6); // Rotate 30 degrees (PI/6 radians)
ctx.fillStyle = 'lightblue';
// Draw relative to the NEW origin (-width/2, -height/2)
ctx.fillRect(-40, -20, 80, 40);
// Reset transform for subsequent drawings
ctx.resetTransform();
}
Drawing Images
Draw existing images (from <img>
tags, other canvases, or created dynamically) onto the canvas.
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
// Draw image once loaded: drawImage(image, dx, dy)
ctx.drawImage(img, 450, 50);
// Draw scaled image: drawImage(image, dx, dy, dWidth, dHeight)
ctx.drawImage(img, 450, 150, img.width / 2, img.height / 2);
// Draw portion (sprite): drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// (Draws area from sx,sy with sWidth,sHeight of source onto dx,dy with dWidth,dHeight on canvas)
ctx.drawImage(img, 10, 10, 30, 30, 450, 250, 60, 60);
};
img.onerror = function() {
console.error('Failed to load image.');
};
img.src = 'path/to/your/image.png'; // Ensure image is loaded before drawing!
}
Pixel Manipulation
Access and modify the raw pixel data of the canvas.
createImageData(width, height)
orcreateImageData(imageData)
: Creates a new, blankImageData
object.getImageData(sx, sy, sw, sh)
: Returns anImageData
object representing the pixel data for a specified rectangle.putImageData(imageData, dx, dy, dirtyX?, dirtyY?, dirtyWidth?, dirtyHeight?)
: Puts pixel data from anImageData
object back onto the canvas.
The ImageData
object has width
, height
, and a data
property (a Uint8ClampedArray
). The data
array contains RGBA values for each pixel sequentially (R, G, B, A, R, G, B, A,...).
function makeGrayscale() {
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
// Ensure something is drawn first
ctx.fillStyle = 'magenta';
ctx.fillRect(10, 300, 100, 50);
try {
const imageData = ctx.getImageData(10, 300, 100, 50);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // Red
data[i + 1] = avg; // Green
data[i + 2] = avg; // Blue
// data[i + 3] is Alpha - leave unchanged
}
ctx.putImageData(imageData, 120, 300); // Draw modified data next to original
console.log('Grayscale filter applied.');
} catch (e) {
console.error('Pixel manipulation error (maybe CORS issue if image is external?):', e);
}
}
}
// makeGrayscale(); // Call this after drawing something
Note: getImageData
is subject to cross-origin security restrictions if external images are drawn onto the canvas without appropriate CORS headers.
Saving and Restoring State
save()
pushes the current drawing state (styles, transformations, clipping regions) onto a stack. restore()
pops the last saved state off the stack, restoring the context to how it was before save()
was called. Essential for isolating drawing operations, especially with transformations.
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 50, 50); // Black square
ctx.save(); // Save current state (black fill)
ctx.fillStyle = 'red';
ctx.translate(70, 0);
ctx.fillRect(0, 0, 50, 50); // Red translated square
ctx.save(); // Save current state (red fill, translated)
ctx.fillStyle = 'blue';
ctx.scale(0.5, 0.5);
ctx.fillRect(0, 60, 50, 50); // Blue, translated, scaled square
ctx.restore(); // Restore to: red fill, translated
ctx.fillRect(0, 120, 50, 50); // Red translated square (state restored)
ctx.restore(); // Restore to: black fill, no translation
ctx.fillRect(140, 0, 50, 50); // Black square (state restored)
}
Animation
Use window.requestAnimationFrame(callback)
for smooth, efficient animations. The browser calls the callback function just before the next repaint.
let x = 0;
let direction = 1;
function drawAnimationFrame() {
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height; // Assuming fixed height for example area
const boxSize = 30;
// Clear the animation area (or entire canvas)
ctx.clearRect(0, canvasHeight - 50, canvasWidth, 50);
// Update position
x += 2 * direction;
if (x + boxSize > canvasWidth || x < 0) {
direction *= -1; // Reverse direction at edges
}
// Draw the object
ctx.fillStyle = 'green';
ctx.fillRect(x, canvasHeight - 40, boxSize, boxSize);
// Request the next frame
requestAnimationFrame(drawAnimationFrame);
}
}
// Start the animation loop
// requestAnimationFrame(drawAnimationFrame);
console.log('Animation setup complete. Uncomment the line above to start.');
Use Cases
- Interactive Games (2D)
- Data Visualization (Charts, Graphs)
- Image Editing and Manipulation Tools
- Creative Coding and Generative Art
- Complex UI elements and Effects
- Video Processing and Effects
6. Web Audio API
Provides a powerful and versatile system for controlling audio processing and synthesis directly in the browser. It allows developers to load, generate, manipulate, and analyze audio with high precision and low latency.
The Audio Context
The core of the Web Audio API is the AudioContext
. It acts as a central controller for the audio graph.
// Create an AudioContext instance
// Needs user interaction (click, etc.) to start/resume in most browsers
let audioContext;
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
console.log('AudioContext state:', audioContext.state);
// Resume if suspended (requires user gesture)
if (audioContext.state === 'suspended') {
document.body.addEventListener('click', () => audioContext.resume(), { once: true });
}
} catch(e) {
console.error('Web Audio API not supported:', e);
}
AudioContext
.
Audio Nodes and the Audio Graph
Audio processing happens through a series of connected Audio Nodes. These nodes form a directed graph, starting from source nodes, passing through processing/effect nodes, and finally reaching the destination node (usually the speakers).
- Source Nodes: Generate or provide audio data.
AudioBufferSourceNode
: Plays audio data from anAudioBuffer
(loaded file).OscillatorNode
: Generates a periodic waveform (sine, square, sawtooth, triangle).MediaElementAudioSourceNode
: Uses audio from an HTML<audio>
or<video>
element.MediaStreamAudioSourceNode
: Uses audio from aMediaStream
(e.g., microphone input).
- Processing/Effect Nodes: Modify the audio signal.
GainNode
: Controls volume.BiquadFilterNode
: Implements filters (lowpass, highpass, etc.).DelayNode
: Introduces a delay.StereoPannerNode
: Positions sound in stereo space.- (and many more...)
- Destination Node: The final output.
audioContext.destination
: Represents the default audio output (speakers).MediaStreamAudioDestinationNode
: Routes audio to aMediaStream
.
Nodes are connected using the connect()
method: sourceNode.connect(processingNode); processingNode.connect(audioContext.destination);
Loading and Playing Audio Files
Use fetch
to get the audio file data as an ArrayBuffer
, then audioContext.decodeAudioData()
to convert it into an AudioBuffer
that can be played by an AudioBufferSourceNode
.
let audioBuffer = null;
async function loadAudio(url) {
if (!audioContext) {
console.error('AudioContext not available.');
return null;
}
try {
console.log(`Fetching audio: ${url}...`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
console.log('Decoding audio data...');
// Use Promise-based decodeAudioData
audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
console.log('Audio loaded and decoded successfully!');
return audioBuffer;
} catch (error) {
console.error('Error loading or decoding audio:', error);
audioBuffer = null;
return null;
}
}
function playAudio() {
if (!audioContext || !audioBuffer) {
console.error('AudioContext not ready or audio buffer not loaded.');
if (audioContext && audioContext.state === 'suspended') {
console.log('AudioContext suspended. Click page to resume.');
}
return;
}
// Ensure context is running
if (audioContext.state === 'running') {
// Create a source node each time you want to play
const source = audioContext.createBufferSource();
source.buffer = audioBuffer; // Assign the loaded buffer
// Connect source directly to destination (speakers)
source.connect(audioContext.destination);
console.log('Starting playback...');
source.start(0); // Start playing immediately (offset 0)
// Optional: Clean up the source node after it finishes playing
source.onended = () => {
console.log('Playback finished.');
source.disconnect(); // Disconnect node
};
} else {
console.log('AudioContext not running. Click page to resume.');
}
}
// Example Usage (load first, then play, likely triggered by user action)
// loadAudio('path/to/your/sound.wav').then(() => {
// document.getElementById('playButton').onclick = playAudio;
// });
console.log('Audio loading/playback functions defined. Call loadAudio() and attach playAudio() to a button.');
Generating Sounds with OscillatorNode
Create simple tones programmatically without loading external files.
function playTone(frequency = 440, duration = 1, type = 'sine') {
if (!audioContext || audioContext.state !== 'running') {
console.error('AudioContext not ready or running.');
return;
}
// Create Oscillator node
const oscillator = audioContext.createOscillator();
oscillator.type = type; // 'sine', 'square', 'sawtooth', 'triangle', 'custom'
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); // Set frequency in Hz (A4 note)
// Optional: Create a Gain node to control volume and prevent clipping
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(0.5, audioContext.currentTime); // Start at half volume
// Fade out smoothly at the end
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration - 0.05);
// Connect oscillator -> gain -> destination
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log(`Playing ${type} tone at ${frequency} Hz for ${duration}s...`);
oscillator.start(audioContext.currentTime); // Start now
oscillator.stop(audioContext.currentTime + duration); // Stop after 'duration' seconds
oscillator.onended = () => {
console.log('Tone finished.');
oscillator.disconnect();
gainNode.disconnect();
};
}
// Example Usage (likely triggered by user action):
// document.getElementById('playToneButton').onclick = () => playTone(440, 0.5, 'sine');
// document.getElementById('playSquareButton').onclick = () => playTone(220, 0.5, 'square');
console.log('playTone function defined. Attach to buttons.');
Applying Simple Effects (Gain & Filter)
Modify the audio signal by inserting processing nodes into the graph.
function playAudioWithEffects() {
if (!audioContext || !audioBuffer || audioContext.state !== 'running') {
console.error('AudioContext/buffer not ready or context not running.');
return;
}
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
// Create Gain node for volume control
const gainNode = audioContext.createGain();
gainNode.gain.value = 0.8; // Set volume (0 to 1+)
// Create BiquadFilter node (Low-pass filter example)
const filterNode = audioContext.createBiquadFilter();
filterNode.type = 'lowpass'; // 'lowpass', 'highpass', 'bandpass', 'notch', etc.
filterNode.frequency.value = 1000; // Cutoff frequency in Hz (lower value = more muffled)
// filterNode.Q.value = 1; // Quality factor (resonance)
// Connect the graph: source -> filter -> gain -> destination
source.connect(filterNode);
filterNode.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('Starting playback with filter and gain...');
source.start(0);
source.onended = () => {
console.log('Playback with effects finished.');
source.disconnect();
filterNode.disconnect();
gainNode.disconnect();
};
}
// Example Usage (assuming audio loaded):
// document.getElementById('playEffectsButton').onclick = playAudioWithEffects;
console.log('playAudioWithEffects function defined. Attach to button.');
Use Cases
- Games: Sound effects, background music, spatial audio.
- Interactive Applications: UI sound feedback, audio visualizations.
- Music Production/Playback: Synthesizers, sequencers, effects processors, sophisticated audio players.
- Accessibility: Auditory feedback for visually impaired users.
- Communication: Real-time audio processing in WebRTC applications.
Key Considerations
- Complexity: Can be significantly more complex than the basic HTML
<audio>
element for simple playback. - Performance: Designed for high performance, but complex audio graphs can still consume CPU resources.
- User Interaction Required: Remember the autoplay policy requires user interaction to start/resume the
AudioContext
. - Garbage Collection: Disconnect nodes when they are no longer needed to allow them to be garbage collected, especially for long-running applications.