Digital Barometer: Vintage Aesthetics, Real-Time Data

This isn't just a static image; it is a fully functional, real-time barometer designed to provide an at-a-glance view of local atmospheric conditions.

It offers immediate, useful data. The design is inspired by traditional, high-end mechanical barometers - the kind you might find in an old library or a Victorian study.
  • Dynamic Mechanical Visuals: The heart of the widget features a set of animated brass-style cogs that provide a subtle, engaging motion, giving the piece a "ticking" life of its own.
  • Real-Time Weather Tracking: Beneath the classic gold bezel, the needle responds to current, live barometric pressure data, allowing you to track changes in the weather as they happen. The smaller needle displays the pressure from 12 hours ago, allowing you to trend how the weather is changing.
  • Integrated Temperature Gauge: Beyond pressure, the widget includes a precise thermometer arc at the bottom, keeping you updated on the current temperature with a clean, unobtrusive display.
  • Real-Time Local Weather: To provide accurate, up-to-the-minute conditions without requiring manual setup, the widget automatically detects your approximate location via your IP address, using that data to fetch precise local temperature and atmospheric pressure readings directly from weather services.
Lightweight & Responsive
Built entirely using scalable vector graphics (SVG) and lightweight code, the barometer is designed to look crisp on any screen size, from desktop monitors to mobile devices, without slowing down page load times.

Whether you are a weather enthusiast or just appreciate the blend of old-world design and clean, modern code, this widget is a subtle way to bring a bit of dynamic, useful motion to the page.
SEANDUFFY.UK STORMY RAIN CHANGE FAIR VERY DRY --

Buy Me A Coffee
Digital Barometer
<style>
.barometer-container {
  width: 100%;
  max-width: 400px;
  margin: 20px auto;
  font-family: system-ui, -apple-system, sans-serif;
}
.barometer-casing {
  background-color: #232323;
  border-radius: 50%;
  padding: 4%;
  box-shadow: 0 16px 38px rgba(0,0,0,0.7), inset 0 0 25px rgba(255,255,255,0.04);
}
.baro-svg {
  width: 100%;
  height: auto;
  display: block;
  filter: drop-shadow(0px 8px 12px rgba(0,0,0,0.45));
}

/* Mechanical Cog Animations */
@keyframes spin-cw { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes spin-ccw { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
.cog-cw { animation: spin-cw 40s linear infinite; }
.cog-ccw { animation: spin-ccw 40s linear infinite; } 

/* Subtle Weather Icon Animation */
@keyframes float-weather {
  0% { transform: translateY(0px); }
  50% { transform: translateY(-1.5px); }
  100% { transform: translateY(0px); }
}
.weather-anim {
  animation: float-weather 4s ease-in-out infinite;
}

/* Smooth transitions */
#baro-needle, #baro-needle-past { transition: transform 2.5s cubic-bezier(0.25, 1, 0.5, 1); }
#thermo-fluid { transition: stroke-dasharray 2s ease-in-out; }
</style> 

<div class="barometer-container">
<div class="barometer-casing">
<svg class="baro-svg" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> 

<defs>
<radialGradient id="dial-face" cx="50%" cy="50%" r="50%">
  <stop offset="60%" stop-color="#fff9eb" />
  <stop offset="100%" stop-color="#e3d09c" />
</radialGradient>
<radialGradient id="cutout-shadow" cx="50%" cy="50%" r="50%">
  <stop offset="75%" stop-color="#141414" />
  <stop offset="100%" stop-color="#000000" />
</radialGradient>
<linearGradient id="gold-bezel" x1="0%" y1="0%" x2="100%" y2="100%">
  <stop offset="0%" stop-color="#f4da70" />
  <stop offset="30%" stop-color="#c19620" />
  <stop offset="55%" stop-color="#fbf0b9" />
  <stop offset="80%" stop-color="#815a04" />
  <stop offset="100%" stop-color="#dcb334" />
</linearGradient>

<linearGradient id="brass-cogs" x1="0%" y1="0%" x2="100%" y2="100%">
  <stop offset="0%" stop-color="#f5e3aa" />
  <stop offset="50%" stop-color="#ceb05a" />
  <stop offset="100%" stop-color="#eadd97" />
</linearGradient>

<linearGradient id="glass-highlight" x1="0%" y1="0%" x2="0%" y2="100%">
  <stop offset="0%" stop-color="rgba(255,255,255,0.45)" />
  <stop offset="100%" stop-color="rgba(255,255,255,0)" />
</linearGradient>

<filter id="drop-shadow" x="-20%" y="-20%" width="140%" height="140%">
  <feDropShadow dx="1.5" dy="3" stdDeviation="1.5" flood-opacity="0.4" />
</filter> 

<path id="text-arc" d="M 44 100 A 56 56 0 0 1 156 100" /> 
<path id="url-arc" d="M 65 100 A 35 35 0 0 1 135 100" fill="none" />

<g id="mech-cog">
  <circle cx="0" cy="0" r="16" stroke="url(#brass-cogs)" stroke-width="3.5" stroke-dasharray="3 3" fill="none"/>
  <circle cx="0" cy="0" r="13" fill="#6e5223" stroke="#402d10" stroke-width="1"/>
  <circle cx="0" cy="0" r="7" fill="#111" />
  <line x1="-13" y1="0" x2="14" y2="0" stroke="url(#brass-cogs)" stroke-width="1.5"/>
  <line x1="0" y1="-13" x2="0" y2="14" stroke="url(#brass-cogs)" stroke-width="1.5"/>
  <circle cx="0" cy="0" r="2.5" fill="#f5e3aa"/>
</g> 

<symbol id="icon-sun" viewBox="0 0 24 24">
  <circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="1.5"/>
  <path d="M12 2v2m0 16v2m10-10h-2M4 12H2m15.5-7.5-1.4 1.4M5.9 17.5l-1.4 1.4m14-14-1.4-1.4M5.9 5.9 4.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</symbol>
<symbol id="icon-cloud" viewBox="0 0 24 24">
  <path d="M17.5 19c2.5 0 4.5-2 4.5-4.5 0-2.4-1.9-4.3-4.2-4.5C17.1 6.6 14.8 4 12 4 8.7 4 6 6.7 6 10c-3 0.2-5 2.7-5 5.5C1 18.5 3.5 21 6.5 21h11z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-rain" viewBox="0 0 24 24">
  <path d="M16 16c2.5 0 4.5-2 4.5-4.5 0-2.4-1.9-4.3-4.2-4.5C15.9 4 14 3 12 3c-2.8 0-5.2 2.2-5.8 5C3.5 8.2 2 10.2 2 12.5 2 15 4 16 6.5 16h9.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
  <path d="M12 18v4m-4-2v4m8-4v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</symbol>
<symbol id="icon-snow" viewBox="0 0 24 24">
  <path d="M12 3v18m-8-9h16m-13.6 6.4 11.3-11.3m0 11.3L5.7 5.7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  <circle cx="12" cy="3" r="1.5" fill="currentColor"/><circle cx="12" cy="21" r="1.5" fill="currentColor"/><circle cx="3" cy="12" r="1.5" fill="currentColor"/><circle cx="21" cy="12" r="1.5" fill="currentColor"/>
</symbol>
<symbol id="icon-storm" viewBox="0 0 24 24">
  <path d="M16 14c2.5 0 4.5-2 4.5-4.5 0-2.4-1.9-4.3-4.2-4.5C15.9 3 14 2 12 2c-2.8 0-5.2 2.2-5.8 5C3.5 6.2 2 8.2 2 10.5 2 13 4 14 6.5 14h9.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
  <path d="M13 14l-3 5h4l-3 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-fog" viewBox="0 0 24 24">
  <path d="M4 10h16M4 14h16M6 18h12M8 6h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</symbol>
</defs> 

<circle cx="100" cy="100" r="96" fill="url(#gold-bezel)" />
<circle cx="100" cy="100" r="89.5" fill="url(#dial-face)" stroke="#4a3707" stroke-width="0.8" /> 

<circle cx="100" cy="100" r="30" fill="url(#cutout-shadow)" stroke="#7a5503" stroke-width="1.5" /> 

<g clip-path="url(#cog-clip)">
  <clipPath id="cog-clip"><circle cx="100" cy="100" r="29" /></clipPath>
  <g transform="translate(100, 100)"><use href="#mech-cog" class="cog-cw" /></g>
  <g transform="translate(76, 114)"><use href="#mech-cog" class="cog-ccw" /></g>
  <g transform="translate(122, 88)"><use href="#mech-cog" class="cog-ccw" /></g>
</g> 

<a href="https://www.seanduffy.uk/?barometer" target="_top" style="text-decoration:none; cursor:pointer;">
  <text font-family="sans-serif" font-size="4.5" font-weight="bold" fill="#8c7747" letter-spacing="0.6" pointer-events="all">
    <textPath href="#url-arc" startOffset="50%" text-anchor="middle">SEANDUFFY.UK</textPath>
  </text>
</a> 

<g class="weather-anim">
  <use id="weather-icon-use" href="" x="93" y="134" width="14" height="14" style="color: #786435;" /> 
</g>

<g id="ticks-group"></g> 

<text font-family="Georgia, serif" font-size="7" font-weight="bold" fill="#362d18">
  <textPath href="#text-arc" startOffset="12%" text-anchor="middle">STORMY</textPath>
  <textPath href="#text-arc" startOffset="31%" text-anchor="middle">RAIN</textPath>
  <textPath href="#text-arc" startOffset="50%" text-anchor="middle">CHANGE</textPath>
  <textPath href="#text-arc" startOffset="69%" text-anchor="middle">FAIR</textPath>
  <textPath href="#text-arc" startOffset="88%" text-anchor="middle">VERY DRY</textPath>
</text> 

<g id="thermometer-group">
  <text id="thermo-readout" x="100" y="161" font-family="sans-serif" font-size="9" font-weight="bold" text-anchor="middle" fill="#b81d1d">--</text> 
  
  <path d="M 58 153 A 61 61 0 0 0 142 153" fill="none" stroke="#d5cbaf" stroke-width="4.5" stroke-linecap="round"/>
  <path id="thermo-fluid" d="M 58 153 A 61 61 0 0 0 142 153" fill="none" stroke="#b81d1d" stroke-width="2.8" stroke-linecap="round" pathLength="100" stroke-dasharray="0 100" />

  <text id="thermo-label-min" x="58" y="166" font-family="sans-serif" font-size="6" font-weight="bold" fill="#444" text-anchor="middle"></text>
  <text id="thermo-label-max" x="142" y="166" font-family="sans-serif" font-size="6" font-weight="bold" fill="#444" text-anchor="middle"></text>
</g> 

<g id="baro-needle-past" transform="rotate(0 100 100)" opacity="0.3">
  <line x1="100" y1="100" x2="100" y2="25" stroke="#333" stroke-width="1.2" stroke-linecap="round" />
  <polygon points="99,33 101,33 100,19" fill="#333" />
</g> 

<g id="baro-needle" transform="rotate(0 100 100)" filter="url(#drop-shadow)">
  <line x1="100" y1="100" x2="100" y2="120" stroke="#111" stroke-width="2.8" stroke-linecap="round" />
  <line x1="100" y1="100" x2="100" y2="23" stroke="#111" stroke-width="2.2" stroke-linecap="round" />
  <polygon points="98.4,33 101.6,33 100,16" fill="#111" />
</g> 

<circle cx="100" cy="100" r="4.5" fill="url(#gold-bezel)" filter="url(#drop-shadow)" />
<circle cx="100" cy="100" r="1.5" fill="#fff9eb" /> 

<path d="M 13 100 A 87 87 0 0 1 187 100 A 87 66 0 0 0 13 100" fill="url(#glass-highlight)" pointer-events="none" /> 

</svg>
</div>
</div> 

<script>
(async function() {
  document.getElementById('thermo-label-min').textContent = '-15\u00B0';
  document.getElementById('thermo-label-max').textContent = '45\u00B0'; 

  const ticksGroup = document.getElementById('ticks-group'); 

  for (let p = 960; p <= 1060; p++) {
    const isMajor = p % 10 === 0;
    const isMinor = p % 2 === 0;
    if (!isMajor && !isMinor) continue; 

    const angle = (p - 1010) * 2.4;
    const y1 = 13;
    const y2 = isMajor ? 21 : 17; 

    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line.setAttribute('x1', '100');
    line.setAttribute('y1', y1.toString());
    line.setAttribute('x2', '100');
    line.setAttribute('y2', y2.toString());
    line.setAttribute('stroke', '#1a1a1a');
    line.setAttribute('stroke-width', isMajor ? '1.3' : '0.65');
    line.setAttribute('transform', `rotate(${angle} 100 100)`);
    ticksGroup.appendChild(line); 

    if (isMajor) {
      const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
      text.setAttribute('x', '100');
      text.setAttribute('y', '29');
      text.setAttribute('font-family', 'sans-serif');
      text.setAttribute('font-size', '5.5');
      text.setAttribute('font-weight', 'bold');
      text.setAttribute('fill', '#222');
      text.setAttribute('text-anchor', 'middle');
      text.setAttribute('transform', `rotate(${angle} 100 100)`);
      text.textContent = p.toString();
      ticksGroup.appendChild(text);
    }
  } 

  try {
    const geoResponse = await fetch('https://get.geojs.io/v1/ip/geo.json');
    if (!geoResponse.ok) throw new Error("Geo failed");
    const geoData = await geoResponse.json(); 

    const weatherResponse = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${geoData.latitude}&longitude=${geoData.longitude}¤t=pressure_msl,temperature_2m,weathercode&hourly=pressure_msl&past_hours=12`);
    if (!weatherResponse.ok) throw new Error("Weather failed");
    const weatherData = await weatherResponse.json(); 

    const pressure = Math.round(weatherData.current.pressure_msl);
    const temperature = Math.round(weatherData.current.temperature_2m);
    const pastPressure = Math.round(weatherData.hourly.pressure_msl[0]);
    const wmoCode = weatherData.current.weathercode; 

    let iconId = '#icon-sun'; 
    if (wmoCode >= 1 && wmoCode <= 3) iconId = '#icon-cloud';
    if (wmoCode >= 45 && wmoCode <= 48) iconId = '#icon-fog'; 
    if (wmoCode >= 51 && wmoCode <= 67) iconId = '#icon-rain';
    if (wmoCode >= 71 && wmoCode <= 77) iconId = '#icon-snow';
    if (wmoCode >= 80 && wmoCode <= 82) iconId = '#icon-rain';
    if (wmoCode >= 85 && wmoCode <= 86) iconId = '#icon-snow';
    if (wmoCode >= 95) iconId = '#icon-storm'; 
    document.getElementById('weather-icon-use').setAttribute('href', iconId); 

    const getPressureRotation = (p) => Math.max(-120, Math.min(120, (p - 1010) * 2.4));
    document.getElementById('baro-needle').setAttribute('transform', `rotate(${getPressureRotation(pressure)} 100 100)`); 
    document.getElementById('baro-needle-past').setAttribute('transform', `rotate(${getPressureRotation(pastPressure)} 100 100)`); 

    document.getElementById('thermo-readout').textContent = temperature + '\u00B0C';
    let tempPercentage = ((temperature - (-15)) / 60) * 100;
    document.getElementById('thermo-fluid').setAttribute('stroke-dasharray', `${Math.max(0, Math.min(100, tempPercentage))} 100`); 

  } catch (err) {
    console.error("Widget Error:", err);
    document.getElementById('thermo-readout').textContent = "ERR";
  }
})();
</script>

Post a Comment

0 Comments

Notice:
Comments are moderated and may not appear immediately. Please keep your comments respectful, and relevant to the post. Spam will not be tolerated. My site. My rules.

Post a Comment (0)