{"id":11888,"date":"2026-09-04T17:14:09","date_gmt":"2026-09-04T16:14:09","guid":{"rendered":"https:\/\/paragliding-in-madeira.com\/weather\/?page_id=11888"},"modified":"2026-09-10T17:25:44","modified_gmt":"2026-09-10T16:25:44","slug":"timelapses","status":"publish","type":"page","link":"https:\/\/paragliding-in-madeira.com\/weather\/timelapses\/","title":{"rendered":"Timelapses"},"content":{"rendered":"\n<figure class=\"wp-block-image alignfull size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1312\" height=\"769\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2026\/09\/1000457937.webp\" alt=\"1000457937\" class=\"wp-image-11889\" srcset=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2026\/09\/1000457937.webp 1312w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2026\/09\/1000457937-300x176.webp 300w\" sizes=\"auto, (max-width: 1312px) 100vw, 1312px\" \/><\/figure>\n\n\n\n<div align=\"center\">\n <figure class=\"wp-block-table is-style-regular\">\n<table class=\"has-fixed-layout\" style=\"border-style: none; border-width: 0px;\">\n<tbody>\n<tr>\n<td class=\"has-text-align-center\" data-align=\"center\"><sub><small>Powered by<\/sub><\/small><\/td>\n<td class=\"has-text-align-center\" data-align=\"center\"><a href=\"https:\/\/www.netmadeira.com\/webcams-madeira\" target=\"_blank\" rel=\"noreferrer noopener\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-868\" style=\"width: 150px;\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/Netmadeira-logo-1.png\" sizes=\"auto, (max-width: 150px) 100vw, 150px\" srcset=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/Netmadeira-logo-1.png 900w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/Netmadeira-logo-1-300x96.png 300w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/Netmadeira-logo-1-768x246.png 768w\" alt=\"\" width=\"150\" height=\"48\" \/><\/a><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<figcaption class=\"wp-element-caption\"><\/figcaption>\n<\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-buttons is-content-justification-center is-layout-flex wp-container-core-buttons-is-layout-0aa01211 wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/\">Home<\/a><\/div>\n\n\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/weather-info\/\">Weather info<\/a><\/div>\n<\/div>\n\n\n\n<p class=\"has-text-align-center wp-block-paragraph\"><a href=\"https:\/\/paragliding-in-madeira.com\/weather\/webcams\/\" title=\"\">Live<\/a> | <a href=\"https:\/\/paragliding-in-madeira.com\/weather\/all-cams\/\" title=\"\">Pictures<\/a> | <a href=\"https:\/\/paragliding-in-madeira.com\/weather\/freeway\" title=\"\">Freeway<\/a> | <a href=\"https:\/\/paragliding-in-madeira.com\/weather\/roads\/\" title=\"\">Roads<\/a><br><strong>Timelapses<\/strong><\/p>\n\n\n\n<div style=\"height:3px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"has-text-align-center wp-block-paragraph\">Visual evolution of local weather conditions<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div style=\"height:22px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- all webcam timelapses Styles -->\n<style>\n    \/* Controls for toggling *\/\n    .view-controls {\n        text-align: center;\n        margin-bottom: 0px;\n    }\n    .view-btn {\n        border-radius: 5px;\n        padding: 8px 16px;\n        margin: 0 5px;\n        cursor: pointer;\n        border: 0px solid #ccc;\n        background-color: #f9f9f9;\n        font-family: sans-serif;\n        opacity: 0.5;\n        transition: background-color 0.3s, opacity 0.3s;\n        outline: none;\n        -webkit-tap-highlight-color: transparent;\n    }\n\n    \/* Active button style *\/\n    .view-btn.active {\n        background-color: #ddd;\n        border-color: #999;\n        font-weight: bold;\n        opacity: 1;\n    }\n\n    \/* Visibility handling *\/\n    .hidden { display: none !important; }\n\n    \/* Custom Timelapse Player Styles *\/\n    .timelapse-container {\n        width: 100%;\n        max-width: 100%;\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        background: #111;\n        border-radius: 6px;\n        overflow: hidden;\n        position: relative;\n    }\n    \n    \/* Loading Overlay Styles *\/\n    .tl-loading-overlay {\n        position: absolute;\n        top: 45%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        background: rgba(0, 0, 0, 0.75);\n        color: #fff;\n        padding: 8px 16px;\n        border-radius: 6px;\n        font-family: sans-serif;\n        font-size: 14px;\n        z-index: 10;\n        pointer-events: none;\n    }\n\n    .tl-frame {\n        width: 100%;\n        height: 245px;\n        object-fit: contain;\n        background: #000;\n        display: block;\n    }\n\n    \/* Custom Timelapse Player Styles - FULLSCREEN FIX *\/\n    .timelapse-container:is(:fullscreen, :-webkit-full-screen) {\n        width: 100vw;\n        height: 100vh;\n        justify-content: space-between;\n    }\n    \n    .timelapse-container:is(:fullscreen, :-webkit-full-screen) .tl-frame {\n        width: 100%;\n        height: calc(100vh - 65px);\n        flex: 1;\n        object-fit: contain;\n    }\n\n    \/* Desktop-specific Fullscreen Rules *\/\n    @media (min-width: 1024px) {\n        .timelapse-container:is(:fullscreen, :-webkit-full-screen) .tl-frame {\n            width: 100vw;\n            max-width: 100vw;\n            height: calc(100vh - 65px);\n            max-height: calc(100vh - 65px);\n            object-fit: contain; \n        }\n    }\n    \n    .timelapse-controls {\n        width: 100%;\n        display: flex;\n        flex-direction: column;\n        padding: 6px 12px;\n        background: #222;\n        color: #fff;\n        box-sizing: border-box;\n        font-family: sans-serif;\n        font-size: 13px;\n        gap: 8px;\n    }\n    \n    .tl-controls-row {\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        width: 100%;\n    }\n    \n    .tl-controls-group {\n        display: flex;\n        align-items: center;\n        gap: 6px;\n    }\n    \n    .tl-controls-group:first-child {\n        flex-grow: 1;\n    }\n\n    .tl-btn {\n        background: #444;\n        color: #fff;\n        border: none;\n        padding: 4px 6px;\n        border-radius: 4px;\n        cursor: pointer;\n        font-size: 12px;\n        outline: none;\n        white-space: nowrap;\n    }\n\/* Fixed min-width prevents layout shift between 1x and 0.5x *\/\n    .tl-btn-speed {\n        min-width: 36px;\n    }\n    .tl-play-btn {\n        min-width: 63px;\n    }\n\n    .tl-scrubber {\n        width: 100%;\n        margin: 0;\n        cursor: pointer;\n    }\n\n    .tl-time-display {\n        font-family: monospace;\n        font-size: 13px;\n        min-width: 45px;\n        text-align: center;\n        flex-grow: 1;\n    }\n\n    \/* NetMadeira styles *\/\n    .iframe-wrapper {\n        width: 100%;\n        overflow: hidden;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n        padding: 0px 0;\n        box-sizing: border-box;\n    }\n    .iframe-container {\n        width: 100vw;\n        max-width: 100%;\n        height: 245px;\n        min-height: 14vh;\n        overflow: hidden;\n        position: relative;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n    }\n    .iframe-container iframe {\n        width: 100%;\n        height: 244px;\n        border: none;\n        transform: scale(1.6);\n        transform-origin: top center;\n        margin-top: -1px;\n        margin-bottom: -2px;\n    }\n<\/style>\n\n\n\n<!-- MASTER CORE ENGINE makes all timelapses work (only needs ONCE per page) -->\n<script>\n    const sheetExportUrlStatus = \"https:\/\/docs.google.com\/spreadsheets\/d\/14WhNcbMIk6FpGkjdJbooNBgMCqYZWCCYI_ROQfXKG-8\/gviz\/tq?tqx=out:csv\";\n\n    window.hasActiveWebcam = false;\n\n    function dismissGlobalWebcamLoader() {\n        const loader = document.getElementById('webcams-global-loader');\n        if (!loader) return;\n        if (window.hasActiveWebcam) {\n            loader.style.display = 'none';\n        } else {\n            loader.innerHTML = 'The source webcam stream is currently offline.<br>Please check back later on.';\n        }\n    }\n    setTimeout(dismissGlobalWebcamLoader, 5000);\n\n    function initWebcamWidget(config) {\n        const executeSetup = () => {\n            const container = document.getElementById(config.containerId);\n            if (!container) return;\n\n            const btnLive = container.querySelector('.btn-live');\n            const btnTimelapse = container.querySelector('.btn-timelapse');\n            const netmadeiraView = container.querySelector('.netmadeira-view');\n            const timelapseView = container.querySelector('.timelapse-view');\n            const iframeContainer = container.querySelector('.iframe-container');\n            const videoPlayer = container.querySelector('.tl-video');\n            \n            const canvas = container.querySelector('.tl-frame');\n            const ctx = canvas ? canvas.getContext('2d') : null;\n            \n            const scrubber = container.querySelector('.tl-scrubber');\n            const playBtn = container.querySelector('.tl-play-btn');\n            const timeDisplay = container.querySelector('.tl-time-display');\n            const dateSelector = container.querySelector('.tl-date-selector');\n            const speedSelector = container.querySelector('.tl-speed-selector');\n            const fullscreenBtn = container.querySelector('.tl-fullscreen-btn');\n            const loadingOverlay = container.querySelector('.tl-loading-overlay');\n            const timelapseContainer = container.querySelector('.timelapse-container');\n\n            let currentDayFolder = \"\";\n            let tlFrames = [];\n            let imageCache = {};\n            let tlIndex = 0;\n            let tlTimer = null;\n            let tlIsPlaying = false;\n            let tlSpeedFactor = 1;\n            let timelapseInitiated = false;\n            let availableMp4Dates = {};\n\n            function switchView(showType) {\n                if (showType === 'live') {\n                    netmadeiraView.classList.remove('hidden');\n                    timelapseView.classList.add('hidden');\n                    btnLive.classList.add('active');\n                    btnTimelapse.classList.remove('active');\n\n                    stopTimelapsePlay();\n                    if (videoPlayer) videoPlayer.pause();\n                } else {\n                    timelapseView.classList.remove('hidden');\n                    netmadeiraView.classList.add('hidden');\n                    btnTimelapse.classList.add('active');\n                    btnLive.classList.remove('active');\n                    \n                    if (!timelapseInitiated) {\n                        timelapseInitiated = true;\n                        loadHistoryDropdown();\n                    }\n                }\n            }\n\n            btnLive.addEventListener('click', () => switchView('live'));\n            btnTimelapse.addEventListener('click', () => switchView('timelapse'));\n\n            async function initializeStatus() {\n                let isOnline = false;\n                try {\n                    const response = await fetch(sheetExportUrlStatus);\n                    if (!response.ok) throw new Error(\"Sheet fetch error\");\n                    const csvText = await response.text();\n                    const row = csvText.split('\\n').find(line => line.includes(config.sheetUrl));\n                    if (row && row.includes('ONLINE')) {\n                        isOnline = true;\n                    }\n                } catch (err) {\n                    console.error(\"Sheet read error. Falling back to active.\", err);\n                    isOnline = true;\n                }\n\n                if (isOnline) {\n                    window.hasActiveWebcam = true;\n                    container.style.display = 'block';\n                    dismissGlobalWebcamLoader();\n                    const iframe = document.createElement('iframe');\n                    iframe.src = config.displayUrl;\n                    iframe.scrolling = \"no\";\n                    iframeContainer.appendChild(iframe);\n                }\n            }\n\n            async function loadHistoryDropdown() {\n                if (loadingOverlay) {\n                    loadingOverlay.textContent = \"Loading timelapse...\";\n                    loadingOverlay.style.display = 'block';\n                }\n                \n                const camName = config.containerId.replace('webcam-', '');\n                const cleanCamName = camName.toLowerCase().replace(\/[^a-z0-9]\/g, '');\n                const now = new Date();\n                const todayYMD = now.getFullYear() + '-' + \n                                 String(now.getMonth() + 1).padStart(2, '0') + '-' + \n                                 String(now.getDate()).padStart(2, '0');\n                                 \n                const yesterdayObj = new Date(now);\n                yesterdayObj.setDate(yesterdayObj.getDate() - 1);\n                const yesterdayYMD = yesterdayObj.getFullYear() + '-' + \n                                     String(yesterdayObj.getMonth() + 1).padStart(2, '0') + '-' + \n                                     String(yesterdayObj.getDate()).padStart(2, '0');\n                                     \n                dateSelector.innerHTML = '';\n                \n                \/\/ 1. Always add \"Today\" option (loads from your server)\n                const optToday = document.createElement('option');\n                optToday.value = todayYMD + \"\/\";\n                optToday.textContent = \"Today\";\n                dateSelector.appendChild(optToday);\n                \n                try {\n                    \/\/ 2. Fetch historical MP4s directly from both GitHub Releases in parallel\n                    const [dailyRes, monthlyRes] = await Promise.all([\n                        fetch(\"https:\/\/api.github.com\/repos\/rubengte\/webcam-timelapse\/releases\/tags\/daily-archive\").catch(() => null),\n                        fetch(\"https:\/\/api.github.com\/repos\/rubengte\/webcam-timelapse\/releases\/tags\/monthly-archive\").catch(() => null)\n                    ]);\n                    \n                    let availableDates = [];\n                    availableMp4Dates = {}; \/\/ Now stores objects: { name: \"...\", tag: \"...\" }\n                    \n                    const processAssets = async (res, tag) => {\n                        if (res && res.ok) {\n                            const releaseData = await res.json();\n\n\/\/ Extract dates ONLY for this specific camera    \nif (releaseData && releaseData.assets) {\n                                releaseData.assets.forEach(asset => {\n\n\/\/ Regex updated to match both cam-YYYY-MM-DD.mp4 AND cam-YYYY-MM.mp4 Looks for ex. \"portela-2026-09-06.mp4\"\n     if (asset.name.endsWith('.mp4')) {\n const match = asset.name.match(\/^(.*)-(\\d{4}-\\d{2}(?:-\\d{2})?)\\.mp4$\/);\n     if (match) {\n const assetCamSlug = match[1].toLowerCase().replace(\/[^a-z0-9]\/g, '');\n                                            const datePart = match[2];\n                                            \n                                            \/\/ Prevent duplicate if an MP4 somehow already exists for today\n                                            if (assetCamSlug === cleanCamName && datePart !== todayYMD) {\n                                                availableDates.push(datePart);\n                                                availableMp4Dates[datePart] = { name: asset.name, tag: tag };\n                                            }\n                                        }\n                                    }\n                                });\n                            }\n                        }\n                    };\n\n                    await processAssets(dailyRes, 'daily-archive');\n                    await processAssets(monthlyRes, 'monthly-archive');\n                    \n\/\/ Keep Yesterday in list even if GitHub Actions hasn't generated the MP4 yet\n                    if (!availableMp4Dates[yesterdayYMD] && !availableDates.includes(yesterdayYMD)) {\n                        availableDates.push(yesterdayYMD);\n                    }\n\n  \/\/ Sort newest to oldest (Standard string sort works perfectly for YYYY-MM-DD vs YYYY-MM)\n    availableDates.sort((a, b) => b.localeCompare(a));\n                    \n\/\/ Populate dropdown with actual existing GitHub videos\n                    availableDates.forEach((day) => {\n                        const opt = document.createElement('option');\n                        opt.value = day + \"\/\";\n                        \n                        const dateParts = day.split('-');\n                        if (dateParts.length === 3) {\n                            \/\/ Daily video\n                            const dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);\n                            opt.textContent = dateObj.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });\n                        } else if (dateParts.length === 2) {\n           \/\/ Monthly video for the drop-down list\nconst dateObj = new Date(dateParts[0], dateParts[1] - 1);\nopt.textContent = dateObj.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }) + \"\";\n                        } else {\n                            opt.textContent = day;\n                        }\n                        dateSelector.appendChild(opt);\n                    });\n                    \n                } catch (err) {\nconsole.error(\"GitHub History unavailable. Showing only Today.\");\n                } finally {\n   \/\/ Always default to Today when switching views\n                    currentDayFolder = todayYMD + \"\/\";\n                    dateSelector.value = currentDayFolder;\n                    loadTimelapseManifest();\n                }\n            }\n\n            async function loadTimelapseManifest() {\n                try {\n                    if (loadingOverlay) loadingOverlay.style.display = 'block';\n                    \n                    const basePath = config.timelapseBaseUrl + currentDayFolder;\n                    const dateStr = currentDayFolder.replace('\/', '');\n                    \n                    const mp4Data = availableMp4Dates[dateStr];\n\n                    \/\/ 1. PAST DATES WITH MP4: Load GitHub Releases MP4 directly\n                    if (mp4Data) {\n                        stopTimelapsePlay(); \n                        canvas.style.display = 'none';\n                        scrubber.style.display = 'none';\n                        playBtn.style.display = 'none';\n                        timeDisplay.style.display = 'none';\n                        speedSelector.style.display = 'inline-block'; \n                        fullscreenBtn.style.display = 'none';                    \n                        videoPlayer.style.display = 'block';\n                        \n                        \/\/ Dynamically pull from daily-archive or monthly-archive based on the saved tag\n                        const videoUrl = `https:\/\/github.com\/rubengte\/webcam-timelapse\/releases\/download\/${mp4Data.tag}\/${mp4Data.name}`;\n                        \n                        videoPlayer.src = videoUrl;\n                        videoPlayer.playbackRate = tlSpeedFactor;\n                        \n                        if (loadingOverlay) loadingOverlay.style.display = 'none';\n                        return;\n                    }\n\n                    \/\/ 2. TODAY OR DATES WITHOUT MP4: Load live server images via canvas\n                    videoPlayer.style.display = 'none';\n                    videoPlayer.pause();\n                    canvas.style.display = 'block';\n                    scrubber.style.display = 'block';\n                    playBtn.style.display = 'inline-block';\n                    timeDisplay.style.display = 'inline-block';\n                    speedSelector.style.display = 'inline-block';\n                    fullscreenBtn.style.display = 'inline-block';\n\n                    const manifestUrl = basePath + \"manifest.json?t=\" + Date.now();\n                    const res = await fetch(manifestUrl);\n\n                    if (!res.ok) {\n                        if (loadingOverlay) loadingOverlay.style.display = 'none';\n                        return;\n                    }\n                    tlFrames = await res.json();\n                    \n                    if (tlFrames.length > 0) {\n                        imageCache = {}; \n                        scrubber.max = tlFrames.length - 1;\n                        tlIndex = tlFrames.length - 1; \n                        scrubber.value = tlIndex;\n                        \n                        if (loadingOverlay) loadingOverlay.style.display = 'none';\n                        renderTimelapseFrame();\n                        stopTimelapsePlay();\n                    } else {\n                        if (loadingOverlay) loadingOverlay.style.display = 'none';\n                    }\n                } catch (err) {\n                    console.error(\"Failed loading media:\", err);\n                    if (loadingOverlay) loadingOverlay.style.display = 'none';\n                }\n            }\n            \n            function drawToCanvas(img) {\n                if (!ctx) return;\n                if (canvas.width !== img.width || canvas.height !== img.height) {\n                    canvas.width = img.width;\n                    canvas.height = img.height;\n                }\n                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n            }\n\n            function renderTimelapseFrame() {\n                if (!tlFrames.length) return;\n                const filename = tlFrames[tlIndex];\n               \n                scrubber.value = tlIndex;\n                const timePart = filename.replace(\/\\.(webp|jpg)$\/i, '').replace('-', ':');\n                timeDisplay.textContent = timePart;\n\n                const imgSrc = config.timelapseBaseUrl + currentDayFolder + filename;\n\n                if (imageCache[filename]) {\n                    drawToCanvas(imageCache[filename]);\n                } else {\n                    const img = new Image();\n                    img.onload = () => {\n                        imageCache[filename] = img;\n                        if (tlFrames[tlIndex] === filename) {\n                            drawToCanvas(img);\n                        }\n                    };\n                    img.src = imgSrc;\n                }\n            }\n\n            function startTimelapsePlay() {\n                if (tlFrames.length > 0 && tlIndex >= tlFrames.length - 1) {\n                    tlIndex = 0;\n                    renderTimelapseFrame();\n                }\n\n                if (tlTimer) clearInterval(tlTimer);\n                tlIsPlaying = true;\n                playBtn.innerHTML = \"Pause &#x2161;\";\n               \n                const speedMs = Math.round(333 \/ tlSpeedFactor);\n                tlTimer = setInterval(() => {\n                    if (tlIndex >= tlFrames.length - 1) {\n                        stopTimelapsePlay();\n                        return;\n                    }\n                    tlIndex++;\n                    renderTimelapseFrame();\n                }, speedMs);\n            }\n\n            function stopTimelapsePlay() {\n                tlIsPlaying = false;\n                if (tlTimer) clearInterval(tlTimer);\n                playBtn.innerHTML = \"Play &#x2009; &#x25B6;\";\n            }\n\n            playBtn.addEventListener('click', () => {\n                tlIsPlaying ? stopTimelapsePlay() : startTimelapsePlay();\n            });\n\n            scrubber.addEventListener('input', (e) => {\n                stopTimelapsePlay();\n                tlIndex = parseInt(e.target.value, 10);\n                renderTimelapseFrame();\n            });\n\n            dateSelector.addEventListener('change', (e) => {\n                stopTimelapsePlay();\n                currentDayFolder = e.target.value; \n                loadTimelapseManifest();\n            });\n\n            speedSelector.addEventListener('change', (e) => {\n                tlSpeedFactor = parseFloat(e.target.value);\n                if (tlIsPlaying) startTimelapsePlay();\n                if (videoPlayer.style.display === 'block') {\n                    videoPlayer.playbackRate = tlSpeedFactor;\n                }\n            });\n\n            fullscreenBtn.addEventListener('click', () => {\n                if (!document.fullscreenElement && !document.webkitFullscreenElement) {\n                    if (timelapseContainer.requestFullscreen) {\n                        timelapseContainer.requestFullscreen();\n                    } else if (timelapseContainer.webkitRequestFullscreen) {\n                        timelapseContainer.webkitRequestFullscreen();\n                    }\n                } else {\n                    if (document.exitFullscreen) {\n                        document.exitFullscreen();\n                    } else if (document.webkitExitFullscreen) {\n                        document.webkitExitFullscreen();\n                    }\n                }\n            });\n\n            initializeStatus();\n        };\n\n        if (document.readyState === 'loading') {\n            document.addEventListener('DOMContentLoaded', executeSetup);\n        } else {\n            setTimeout(executeSetup, 0);\n        }\n    }\n<\/script>\n\n\n\n<!-- loader text -->\n<div id=\"webcams-global-loader\" style=\"text-align: center; padding: 25px 10px; font-family: sans-serif; color: #666; font-size: 15px;\">\n    Loading some timelapse magic&#8230;\n<\/div>\n\n\n\n<!-- Portela Real time and timelapse widget -->\n<!-- 1 LOCATION CONFIG - edit start -->\n<script>\n    initWebcamWidget({\n        containerId: 'webcam-portela',\n        sheetUrl: 'https:\/\/www.netmadeira.com\/webcams-madeira\/portela-porto-da-cruz',\n        displayUrl: 'https:\/\/www.netmadeira.com\/webcams\/show\/netmadeira\/portela-porto-da-cruz',\n        timelapseBaseUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/webcam-timelapse\/portela-frames\/'\n    });\n<\/script>\n<!-- Edit ends below with div id and title -->\n<!-- 2 WEBCAM CARD STRUCTURE -->\n<div id=\"webcam-portela\" class=\"webcam-card\" style=\"display: none;\">\n    \n<!-- Title Section -->\n<div style=\"text-align: center; margin-bottom: 12px;\">\n    <h3 style=\"margin: 0; font-size: 1.2em; color: #333;\">Portela<\/h3>\n <p style=\"margin: 0; font-size: 0.9em; color: #666;\"><\/p>\n<\/div>\n\n    <!-- View Toggle Buttons -->\n    <div class=\"view-controls\">\n        <button class=\"view-btn btn-live active\">Live Webcam<\/button>\n        <button class=\"view-btn btn-timelapse\">Daytime Timelapse<\/button>\n    <\/div>\n\n    <!-- Timelapse View -->\n    <div class=\"timelapse-view hidden\" align=\"center\">\n        <div class=\"timelapse-container\">\n            <!-- Loading Overlay -->\n            <div class=\"tl-loading-overlay\" style=\"display: none;\">Loading images&#8230;<\/div>\n\n<!-- Video Player for historical days (Cached) -->\n <video class=\"tl-video\" controls muted playsinline style=\"display: none; width: 100%; max-height: 80vh; background: #000;\"><\/video>\n            \n <!-- Canvas for today's live images -->\n <canvas class=\"tl-frame\"><\/canvas>\n            \n <div class=\"timelapse-controls\">\n \n<!-- Top Row: Full Width Slider -->\n <input type=\"range\" class=\"tl-scrubber\" min=\"0\" max=\"0\" value=\"0\" step=\"1\">\n                \n                <!-- Bottom Row: Controls -->\n                <div class=\"tl-controls-row\">\n                    <div class=\"tl-controls-group\">\n                        <button type=\"button\" class=\"tl-btn tl-play-btn\">Play &#x2009; &#x25B6;<\/button>\n                        <span class=\"tl-time-display\">00:00<\/span>\n                    <\/div>\n                    \n                    <div class=\"tl-controls-group\">\n                        <select class=\"tl-date-selector\" style=\"background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 2px 5px; font-size: 12px; outline: none; cursor: pointer;\">\n                            <option value=\"today\">Today<\/option>\n                        <\/select>\n                        \n <!-- Video Speed Button -->                                              <button type=\"button\" class=\"tl-btn tl-btn-speed\" onclick=\"\n                            const speeds = ['0.5', '0.7', '1', '2', '3'];\n                            let current = speeds.indexOf(this.dataset.val || '1');\n                            let next = speeds[(current + 1) % speeds.length];\n                            this.dataset.val = next;\n                            this.textContent = next + 'x';\n                            let hiddenInput = this.nextElementSibling;\n                            hiddenInput.value = next;\n                            hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));\n                        \" data-val=\"1\" title=\"Click to cycle speed\">1x<\/button>\n                        <input type=\"hidden\" class=\"tl-speed-selector\" value=\"1\">         \n                        <button type=\"button\" class=\"tl-btn tl-fullscreen-btn\" title=\"Fullscreen\">\u26f6<\/button>\n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <!-- Live NetMadeira View -->\n    <div class=\"netmadeira-view iframe-wrapper\">\n        <div class=\"iframe-container\">\n            <!-- Iframe injected dynamically if online -->\n        <\/div>\n    <\/div>\n\n<\/div>\n\n\n\n<div style=\"height:3px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- Ponta do Sol Real time and timelapse widget -->\n<!-- 1 LOCATION CONFIG - edit start -->\n<script>\n    initWebcamWidget({\n        containerId: 'webcam-pontasol',\n        sheetUrl: 'https:\/\/www.netmadeira.com\/webcams-madeira\/ponta-do-sol',\n        displayUrl: 'https:\/\/www.netmadeira.com\/webcams\/show\/netmadeira\/ponta-do-sol',\n        timelapseBaseUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/webcam-timelapse\/ponta-sol-frames\/'\n    });\n<\/script>\n<!-- Edit ends below with div id and title -->\n<!-- 2 WEBCAM CARD STRUCTURE -->\n<div id=\"webcam-pontasol\" class=\"webcam-card\" style=\"display: none;\">\n    \n<!-- Title Section -->\n<div style=\"text-align: center; margin-bottom: 12px;\">\n    <h3 style=\"margin: 0; font-size: 1.2em; color: #333;\">Ponta do Sol<\/h3>\n <p style=\"margin: 0; font-size: 0.9em; color: #666;\"><\/p>\n<\/div>\n\n    <!-- View Toggle Buttons -->\n    <div class=\"view-controls\">\n        <button class=\"view-btn btn-live active\">Live Webcam<\/button>\n        <button class=\"view-btn btn-timelapse\">Daytime Timelapse<\/button>\n    <\/div>\n\n    <!-- Timelapse View -->\n    <div class=\"timelapse-view hidden\" align=\"center\">\n        <div class=\"timelapse-container\">\n            <!-- Loading Overlay -->\n            <div class=\"tl-loading-overlay\" style=\"display: none;\">Loading images&#8230;<\/div>\n            \n<!-- Video Player for historical days (Cached) -->\n <video class=\"tl-video\" controls muted playsinline style=\"display: none; width: 100%; max-height: 80vh; background: #000;\"><\/video>\n            \n <!-- Canvas for today's live images -->\n            <canvas class=\"tl-frame\"><\/canvas>\n            \n            <div class=\"timelapse-controls\">\n                <!-- Top Row: Full Width Slider -->\n                <input type=\"range\" class=\"tl-scrubber\" min=\"0\" max=\"0\" value=\"0\" step=\"1\">\n                \n                <!-- Bottom Row: Controls -->\n                <div class=\"tl-controls-row\">\n                    <div class=\"tl-controls-group\">\n                        <button type=\"button\" class=\"tl-btn tl-play-btn\">Play &#x2009; &#x25b6;<\/button>\n                        <span class=\"tl-time-display\">00:00<\/span>\n                    <\/div>\n                    \n                    <div class=\"tl-controls-group\">\n                        <select class=\"tl-date-selector\" style=\"background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 2px 5px; font-size: 12px; outline: none; cursor: pointer;\">\n                            <option value=\"today\">Today<\/option>\n                        <\/select>\n                        \n   <!-- Video Speed Button -->                                              <button type=\"button\" class=\"tl-btn tl-btn-speed\" onclick=\"\n                            const speeds = ['0.5', '0.7', '1', '2', '3'];\n                            let current = speeds.indexOf(this.dataset.val || '1');\n                            let next = speeds[(current + 1) % speeds.length];\n                            this.dataset.val = next;\n                            this.textContent = next + 'x';\n                            let hiddenInput = this.nextElementSibling;\n                            hiddenInput.value = next;\n                            hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));\n                        \" data-val=\"1\" title=\"Click to cycle speed\">1x<\/button>\n                        <input type=\"hidden\" class=\"tl-speed-selector\" value=\"1\">\n                        \n                        <button type=\"button\" class=\"tl-btn tl-fullscreen-btn\" title=\"Fullscreen\">\u26f6<\/button>\n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <!-- Live NetMadeira View -->\n    <div class=\"netmadeira-view iframe-wrapper\">\n        <div class=\"iframe-container\">\n            <!-- Iframe injected dynamically if online -->\n        <\/div>\n    <\/div>\n\n<\/div>\n\n\n\n<div style=\"height:3px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- Ribeira Brava Real time and timelapse widget -->\n<!-- 1 LOCATION CONFIG - edit start -->\n<script>\n    initWebcamWidget({\n   containerId: 'webcam-ribeirabrava',\n        sheetUrl: 'https:\/\/www.netmadeira.com\/webcams-madeira\/ribeira-brava',\n        displayUrl: 'https:\/\/www.netmadeira.com\/webcams\/show\/netmadeira\/ribeira-brava',\n        timelapseBaseUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/webcam-timelapse\/ribeira-brava-frames\/'\n    });\n<\/script>\n<!-- Edit ends below with div id and title -->\n<!-- 2 WEBCAM CARD STRUCTURE -->\n<!-- Widget Container -->\n<div id=\"webcam-ribeirabrava\" class=\"webcam-card\" style=\"display: none;\">\n\n<!-- Title Section -->\n<div style=\"text-align: center; margin-bottom: 12px;\">\n    <h3 style=\"margin: 0; font-size: 1.2em; color: #333;\">Ribeira Brava<\/h3>\n <p style=\"margin: 0; font-size: 0.9em; color: #666;\"><\/p>\n<\/div>\n\n<!-- View Toggle Buttons -->\n    <div class=\"view-controls\">\n        <button class=\"view-btn btn-live active\">Live Webcam<\/button>\n        <button class=\"view-btn btn-timelapse\">Daytime Timelapse<\/button>\n    <\/div>\n\n    <!-- Timelapse View -->\n    <div class=\"timelapse-view hidden\" align=\"center\">\n        <div class=\"timelapse-container\">\n            <!-- Loading Overlay -->\n            <div class=\"tl-loading-overlay\" style=\"display: none;\">Loading images&#8230;<\/div>\n            \n<!-- Video Player for historical days (Cached) -->\n <video class=\"tl-video\" controls muted playsinline style=\"display: none; width: 100%; max-height: 80vh; background: #000;\"><\/video>\n            \n <!-- Canvas for today's live images -->\n            <canvas class=\"tl-frame\"><\/canvas>\n            \n            <div class=\"timelapse-controls\">\n                <!-- Top Row: Full Width Slider -->\n                <input type=\"range\" class=\"tl-scrubber\" min=\"0\" max=\"0\" value=\"0\" step=\"1\">\n                \n                <!-- Bottom Row: Controls -->\n                <div class=\"tl-controls-row\">\n                    <div class=\"tl-controls-group\">\n                        <button type=\"button\" class=\"tl-btn tl-play-btn\">Play &#x2009; &#x25b6;<\/button>\n                        <span class=\"tl-time-display\">00:00<\/span>\n                    <\/div>\n                    \n                    <div class=\"tl-controls-group\">\n                        <select class=\"tl-date-selector\" style=\"background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 2px 5px; font-size: 12px; outline: none; cursor: pointer;\">\n                            <option value=\"today\">Today<\/option>\n                        <\/select>\n                        \n   <!-- Video Speed Button -->                                              <button type=\"button\" class=\"tl-btn tl-btn-speed\" onclick=\"\n                            const speeds = ['0.5', '0.7', '1', '2', '3'];\n                            let current = speeds.indexOf(this.dataset.val || '1');\n                            let next = speeds[(current + 1) % speeds.length];\n                            this.dataset.val = next;\n                            this.textContent = next + 'x';\n                            let hiddenInput = this.nextElementSibling;\n                            hiddenInput.value = next;\n                            hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));\n                        \" data-val=\"1\" title=\"Click to cycle speed\">1x<\/button>\n                        <input type=\"hidden\" class=\"tl-speed-selector\" value=\"1\">\n                        \n                        <button type=\"button\" class=\"tl-btn tl-fullscreen-btn\" title=\"Fullscreen\">\u26f6<\/button>\n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <!-- Live NetMadeira View -->\n    <div class=\"netmadeira-view iframe-wrapper\">\n        <div class=\"iframe-container\">\n            <!-- Iframe injected dynamically if online -->\n        <\/div>\n    <\/div>\n\n<\/div>\n\n\n\n<div style=\"height:3px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- Pico do Arieiro Real time and timelapse widget -->\n<!-- 1 LOCATION CONFIG - edit start -->\n<script>\n    initWebcamWidget({\n   containerId: 'webcam-picoarieiro',\n        sheetUrl: 'https:\/\/www.netmadeira.com\/webcams-madeira\/pico-do-arieiro',\n        displayUrl: 'https:\/\/www.netmadeira.com\/webcams\/show\/netmadeira\/pico-do-arieiro',\n        timelapseBaseUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/webcam-timelapse\/pico-arieiro-frames\/'\n    });\n<\/script>\n<!-- Edit ends below with div id and title -->\n<!-- 2 WEBCAM CARD STRUCTURE -->\n<!-- Widget Container -->\n<div id=\"webcam-picoarieiro\" class=\"webcam-card\" style=\"display: none;\">\n\n<!-- Title Section -->\n<div style=\"text-align: center; margin-bottom: 12px;\">\n    <h3 style=\"margin: 0; font-size: 1.2em; color: #333;\">Pico do Arieiro<\/h3>\n <p style=\"margin: 0; font-size: 0.9em; color: #666;\"><\/p>\n<\/div>\n\n<!-- View Toggle Buttons -->\n    <div class=\"view-controls\">\n        <button class=\"view-btn btn-live active\">Live Webcam<\/button>\n        <button class=\"view-btn btn-timelapse\">Daytime Timelapse<\/button>\n    <\/div>\n\n    <!-- Timelapse View -->\n    <div class=\"timelapse-view hidden\" align=\"center\">\n        <div class=\"timelapse-container\">\n            <!-- Loading Overlay -->\n            <div class=\"tl-loading-overlay\" style=\"display: none;\">Loading images&#8230;<\/div>\n            \n<!-- Video Player for historical days (Cached) -->\n <video class=\"tl-video\" controls muted playsinline style=\"display: none; width: 100%; max-height: 80vh; background: #000;\"><\/video>\n            \n <!-- Canvas for today's live images -->\n            <canvas class=\"tl-frame\"><\/canvas>\n            \n            <div class=\"timelapse-controls\">\n                <!-- Top Row: Full Width Slider -->\n                <input type=\"range\" class=\"tl-scrubber\" min=\"0\" max=\"0\" value=\"0\" step=\"1\">\n                \n                <!-- Bottom Row: Controls -->\n                <div class=\"tl-controls-row\">\n                    <div class=\"tl-controls-group\">\n                        <button type=\"button\" class=\"tl-btn tl-play-btn\">Play &#x2009; &#x25b6;<\/button>\n                        <span class=\"tl-time-display\">00:00<\/span>\n                    <\/div>\n                    \n                    <div class=\"tl-controls-group\">\n                        <select class=\"tl-date-selector\" style=\"background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 2px 5px; font-size: 12px; outline: none; cursor: pointer;\">\n                            <option value=\"today\">Today<\/option>\n                        <\/select>\n                        \n   <!-- Video Speed Button -->                                              <button type=\"button\" class=\"tl-btn tl-btn-speed\" onclick=\"\n                            const speeds = ['0.5', '0.7', '1', '2', '3'];\n                            let current = speeds.indexOf(this.dataset.val || '1');\n                            let next = speeds[(current + 1) % speeds.length];\n                            this.dataset.val = next;\n                            this.textContent = next + 'x';\n                            let hiddenInput = this.nextElementSibling;\n                            hiddenInput.value = next;\n                            hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));\n                        \" data-val=\"1\" title=\"Click to cycle speed\">1x<\/button>\n                        <input type=\"hidden\" class=\"tl-speed-selector\" value=\"1\">\n                        \n                        <button type=\"button\" class=\"tl-btn tl-fullscreen-btn\" title=\"Fullscreen\">\u26f6<\/button>\n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <!-- Live NetMadeira View -->\n    <div class=\"netmadeira-view iframe-wrapper\">\n        <div class=\"iframe-container\">\n            <!-- Iframe injected dynamically if online -->\n        <\/div>\n    <\/div>\n\n<\/div>\n\n\n\n<div style=\"height:44px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Powered by Live | Pictures | Freeway | RoadsTimelapses Visual evolution of local weather conditions Loading some timelapse magic&#8230; Portela Live Webcam Daytime Timelapse Loading images&#8230; Play &#x2009; &#x25B6; 00:00 Today 1x \u26f6 Ponta do Sol Live Webcam Daytime Timelapse Loading images&#8230; Play &#x2009; &#x25b6; 00:00 Today 1x \u26f6 Ribeira Brava Live Webcam Daytime Timelapse [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"slim_seo":{"title":"Madeira Webcam timelapses","description":"Some of Madeira's webcams in timelapse. Watch daytime evolution of local conditions around the island","facebook_image":"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2026\/09\/1000457937.webp","twitter_image":"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2026\/09\/1000457937.webp","noindex":"1"},"footnotes":""},"class_list":["post-11888","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/11888","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/comments?post=11888"}],"version-history":[{"count":43,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/11888\/revisions"}],"predecessor-version":[{"id":11995,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/11888\/revisions\/11995"}],"wp:attachment":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/media?parent=11888"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}