Nature Ambient Background
:root {
/* Lighter, more organic forest tones */
–forest-0: #1a2f26; /* Deep Moss */
–forest-1: #243d32; /* Forest Green */
–forest-2: #2f4f41; /* Pine */
–text-main: #f0f7f4;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: transparent;
font-family: ‘Segoe UI’, system-ui, sans-serif;
overflow-x: hidden;
color: var(–text-main);
}
/* — BACKGROUND LAYER (Deepest) — */
#nature-bg {
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
overflow: hidden;
background-color: var(–forest-0);
/* ADDED: Soft blur to background */
filter: blur(5px);
/* ADDED: Slight scale to prevent blurred edges from pulling in white borders */
transform: scale(1.02);
}
#park-fx {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
/* — VIGNETTE LAYER (Middle Background) — */
.vignette-overlay {
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
/* Subtle texture noise */
background-image: url(“data:image/svg+xml,%3Csvg viewBox=’0 0 200 200′ xmlns=’http://www.w3.org/2000/svg’%3E%3Cfilter id=’noiseFilter’%3E%3CfeTurbulence type=’fractalNoise’ baseFrequency=’0.8′ numOctaves=’3′ stitchTiles=’stitch’/%3E%3C/filter%3E%3Crect width=’100%25′ height=’100%25′ filter=’url(%23noiseFilter)’ opacity=’0.04’/%3E%3C/svg%3E”);
/* Lighter darkening at edges, more green/blue tint */
background: radial-gradient(
circle at 50% 50%,
transparent 10%,
rgba(15, 35, 25, 0.1) 60%,
rgba(10, 25, 20, 0.6) 100%
);
}
/**
* CALMING NATURE ANIMATION
* Updated Physics: Real 3D flutter, sway, and variable air resistance.
*/
(() => {
const reduceMotion = matchMedia(‘(prefers-reduced-motion: reduce)’).matches;
const canvas = document.getElementById(‘park-fx’);
const ctx = canvas.getContext(‘2d’, { alpha: false });
let dpr = Math.min(2, window.devicePixelRatio || 1);
let width, height;
// — CONFIGURATION —
const CFG = {
leaves: {
count: 45,
types: 6,
minSize: 18,
maxSize: 35,
// Brighter, more vibrant leaf colors
colors: [
{h: 125, s: 45, l: 48}, // Fresh Green
{h: 110, s: 50, l: 38}, // Olive
{h: 145, s: 40, l: 45}, // Teal Green
{h: 85, s: 45, l: 42} // Lime-ish Green
]
},
flies: {
count: 35,
color: ‘200, 255, 180’ // Brighter, yellower light
}
};
// — 1. ASSET GENERATION (Pre-rendering) —
const leafSprites = [];
function createLeafSprite() {
const s = CFG.leaves.maxSize * dpr * 2.5;
const c = document.createElement(‘canvas’);
c.width = s;
c.height = s;
const cx = c.getContext(‘2d’);
const hueBase = CFG.leaves.colors[Math.floor(Math.random() * CFG.leaves.colors.length)];
const hue = hueBase.h + (Math.random() * 15 – 7);
const sat = hueBase.s;
const lig = hueBase.l;
cx.translate(s/2, s/2);
const L_W = s * 0.28;
const L_H = s * 0.5;
// — Draw Leaf Body —
cx.beginPath();
cx.moveTo(0, -L_H);
cx.bezierCurveTo(L_W * 1.2, -L_H * 0.3, L_W * 1.0, L_H * 0.6, 0, L_H * 0.8);
cx.bezierCurveTo(-L_W * 1.0, L_H * 0.6, -L_W * 1.2, -L_H * 0.3, 0, -L_H);
cx.closePath();
// — Complex Gradient —
const g = cx.createLinearGradient(0, -L_H, 0, L_H);
g.addColorStop(0, `hsl(${hue}, ${sat}%, ${lig+8}%)`);
g.addColorStop(0.5, `hsl(${hue}, ${sat}%, ${lig}%)`);
g.addColorStop(1, `hsl(${hue-15}, ${sat+10}%, ${lig-15}%)`);
cx.fillStyle = g;
cx.fill();
// — Veins —
cx.save();
cx.clip();
cx.strokeStyle = `hsla(${hue}, ${sat-10}%, ${lig+15}%, 0.4)`;
cx.lineWidth = 1.5 * dpr;
cx.beginPath();
cx.moveTo(0, -L_H * 0.95);
cx.quadraticCurveTo(L_W*0.05, 0, 0, L_H * 0.9);
cx.stroke();
cx.lineWidth = 0.8 * dpr;
cx.strokeStyle = `hsla(${hue}, ${sat-10}%, ${lig+15}%, 0.3)`;
for(let j=0; j<6; j++) {
const yStart = -L_H * 0.7 + (j * (L_H * 0.25));
cx.beginPath();
cx.moveTo(0, yStart);
cx.quadraticCurveTo(L_W*0.4, yStart – L_H*0.1, L_W*0.8, yStart – L_H*0.25);
cx.stroke();
cx.beginPath();
cx.moveTo(0, yStart + L_H*0.05);
cx.quadraticCurveTo(-L_W*0.4, yStart – L_H*0.05, -L_W*0.8, yStart – L_H*0.2);
cx.stroke();
}
cx.restore();
// — Stem —
cx.beginPath();
cx.strokeStyle = `hsl(${hue-15}, ${sat+5}%, ${lig-10}%)`;
cx.lineWidth = 2 * dpr;
cx.lineCap = 'round';
cx.moveTo(0, L_H * 0.78);
cx.lineTo(0, L_H * 1.1);
cx.stroke();
return c;
}
for(let i=0; i {
return Math.sin(x * 0.0015 + t) * Math.cos(y * 0.002 + t*0.3);
};
function init() {
resize();
window.addEventListener(‘resize’, resize);
// Create Fireflies
for(let i=0; i<CFG.flies.count; i++) {
items.push({
type: 'fly',
x: Math.random() * width,
y: Math.random() * height,
z: Math.random() * 0.8 + 0.2,
size: Math.random() * 1.5 + 0.5,
phase: Math.random() * Math.PI * 2,
speed: Math.random() * 0.5 + 0.2
});
}
// Create Leaves
for(let i=0; i a.z – b.z);
for (const p of items) {
// — FIREFLY LOGIC —
if (p.type === ‘fly’) {
const nX = noise(p.x, p.y, time * 0.15) * 20;
p.x += (Math.cos(time * p.speed * 0.3 + p.phase) * 0.5 + nX * 0.05) * p.z;
p.y -= (Math.sin(time * p.speed * 0.3 + p.phase) * 0.5 + 0.15) * p.z;
if(p.x width + 20) p.x = -20;
if(p.y height + 20) p.y = -20;
const opacity = (Math.sin(time * 1.5 + p.phase) + 1) / 2 * 0.5 + 0.1;
const r = p.size * p.z * dpr;
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r * 5);
g.addColorStop(0, `rgba(${CFG.flies.color}, ${opacity})`);
g.addColorStop(1, `rgba(${CFG.flies.color}, 0)`);
ctx.globalCompositeOperation = ‘screen’;
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(p.x, p.y, r * 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = `rgba(240, 255, 230, ${opacity + 0.3})`;
ctx.beginPath();
ctx.arc(p.x, p.y, r * 0.6, 0, Math.PI * 2);
ctx.fill();
ctx.globalCompositeOperation = ‘source-over’;
}
// — LEAF LOGIC (2D Fluttering) —
else if (p.type === ‘leaf’) {
// 1. Calculate Fall Speed (Constant base + depth variance)
// UPDATED: Much slower fall speed
const fallSpeed = (4 + p.z * 8);
// 2. Side-to-Side Sway (Flutter)
// UPDATED: Slower sway frequency
p.swayPhase += p.swaySpeed * dt * 0.3;
// Reduced sway amplitude for realism at low speeds
const sway = Math.sin(p.swayPhase) * 10 * p.z;
// 3. Global Wind + Turbulence
const turbulence = noise(p.x, p.y, time * 0.1);
// UPDATED: Reduced wind force significantly
const wind = (2 + turbulence * 5) * p.z;
// Apply Motions
p.x += (wind + sway) * dt;
p.y += fallSpeed * dt;
// Gentle rotation drift (2D only)
// UPDATED: Slower rotation
p.rotation += p.rotationSpeed * dt * 0.2;
// Respawn
if (p.y > height + 50) {
p.y = -50;
p.x = Math.random() * width;
p.rotation = Math.random() * Math.PI * 2;
}
if (p.x > width + 50) p.x = -50;
if (p.x < -50) p.x = width + 50;
// Draw
const s = 1.0 * p.z;
ctx.globalAlpha = Math.max(0.4, p.z * 1.0); // Increased visibility for lighter bg
ctx.save();
ctx.translate(p.x, p.y);
// Apply 2D Rotation
ctx.rotate(p.rotation);
// Standard Scaling (No 3D flip)
ctx.scale(s/dpr/2, s/dpr/2);
ctx.drawImage(p.sprite, -p.sprite.width/2, -p.sprite.height/2);
ctx.restore();
ctx.globalAlpha = 1.0;
}
}
requestAnimationFrame(loop);
}
init();
})();
The Park in the News
Articles that put a spotlight on the beauty of James and Ethel Gray Park and the work done to maintain it.
All
Community
Events
Wildlife
Events
29 May 2026
Official James and Ethel Gray Park event listing for 5 September 2026, including 18+ age guidance, venue details, line-up context and Howler ticket link.
Events
22 Aug 2025
Fun run and walk hosted in the park to raise funds and awareness for early learning, positioning the park as a family-friendly venue for purpose-led fitness.
Community
10 Jun 2025
Xola recalls weekends at James & Ethel Gray Park with his daughters, highlighting the park as a go-to green space for Joburg families.
Community
27 Feb 2025
Listicle names the park a peaceful family stop with a dedicated play area, reinforcing its role as a safe, central outing.
Community
2 Dec 2024
Round-up recommends the park as an easy, scenic day out with space for active families during school break.
Community
Undated
City Parks overview highlights the park’s river setting, lawns and trails as part of Johannesburg’s flagship green lungs.
Community
5 Jan 2015
Community-led conservancy plan links James & Ethel Gray Park and Sandspruit stream to create a protected green corridor.
Community
10 Aug 2023
Column praises the community effort that restored James & Ethel Gray Park’s paths, views and daily running routes.
Community
29 Jun 2023
A donor-funded project will expand capacity, improve accessibility and hygiene, and modernise the ablution block at the park’s lower section.
Community
10 Jul 2022
Feature points to the park’s adoption and partnerships as proof that upgrades and community use can scale city-wide.
Community
15 Jun 2022
Guide recommends the park’s lawns and easy access for a free, family picnic day in Joburg.
Wildlife
9 Jun 2022
City Parks and the Foundation expanded indigenous canopy at the park, underscoring its role in Joburg’s urban ecology.
Events
2 Nov 2021
Fencing, 24-hour security and regular clean-ups formalised through public-private partnerships to keep the park safe and welcoming.
Events
1 Oct 2021
Foundation volunteers cleared the river inside James & Ethel Gray Park to protect local waterways and improve the visitor experience.
Community
24 Jun 2021
Travel feature points visitors to James & Ethel Gray Park as part of Joburg’s leafy urban fabric near Melrose.
Community
10 Aug 2020
JEGPF and JCPZ partner to boost safety with perimeter fencing, better lighting, clear sightlines and regular volunteer clean-ups.
Community
10 Aug 2020
Sunil Geness thanks 150+ volunteers and asks park users to pitch in during Women’s Month clean-ups to keep the park pristine.
Community
10 Aug 2020
Report highlights JEGPF’s volunteer-led safety programme—fencing, lighting and weekly maintenance—as a blueprint for city parks.
Community
18 Sep 2019
Cochrane fencing and partner support to fence and maintain James & Ethel Gray Park, improving safety and access.
Community
26 Nov 2014
Background on the park’s namesakes and community stewardship that keeps the Melrose green space welcoming.
:root {
–forest-0: #1a2f26;
–forest-1: #243d32;
–forest-2: #2f4f41;
–card-bg: #f4efe6;
–card-hover: #fffbfa;
–card-border: #e0d8ca;
–icon-border: #c2bbae;
–subtle-rule: #c2bbae;
–text-heading: #2d3e35;
–text-body: #4a5951;
–radius-book: 4px;
–shadow-base: 2px 4px 12px rgba(0,0,0,0.2);
–shadow-hover: 4px 10px 25px rgba(0,0,0,0.3);
–font-serif: Georgia, serif;
–transition-book: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
html, body {
min-height: 100%;
height: auto;
overflow-x: hidden;
background-color: var(–forest-0);
font-family: var(–font-serif);
}
body.page-id-228,
body.page-id-228 .site,
body.page-id-228 .site-content,
body.page-id-228 #content,
body.page-id-228 #page,
body.page-id-228 #primary,
body.page-id-228 .ast-container,
body.page-id-228 article.page,
body.page-id-228 .entry-content {
background: transparent !important;
}
body.page-id-228 #nature-bg { z-index: 0 !important; }
body.page-id-228 .vignette-overlay { z-index: 1 !important; }
#articles, #articles *, #articles *::before, #articles *::after { box-sizing: border-box; }
#articles {
position: relative;
z-index: 2;
padding: 4rem 0 6rem;
background: transparent;
font-family: var(–font-serif);
color: var(–text-body);
}
#articles .container { width: min(1400px, 92vw); margin-inline: auto; }
#articles .articles-panel {
position: relative;
padding: clamp(28px, 5vw, 56px);
border-radius: var(–radius-book);
background:
linear-gradient(135deg, rgba(255, 251, 250, 0.72), rgba(244, 239, 230, 0.48)),
linear-gradient(180deg, rgba(244, 239, 230, 0.62), rgba(244, 239, 230, 0.42));
border: 1px solid rgba(255, 255, 255, 0.78);
box-shadow:
0 28px 70px rgba(0, 0, 0, 0.30),
0 0 0 1px rgba(255, 255, 255, 0.30),
inset 0 1px 0 rgba(255, 255, 255, 0.90),
inset 0 -24px 45px rgba(45, 62, 53, 0.09);
-webkit-backdrop-filter: blur(26px) saturate(1.28);
backdrop-filter: blur(26px) saturate(1.28);
overflow: hidden;
}
#articles .articles-panel::before {
content: “”;
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
background:
radial-gradient(circle at 12% 9%, rgba(255, 255, 255, 0.72), transparent 30%),
radial-gradient(circle at 92% 18%, rgba(255, 255, 255, 0.42), transparent 25%),
radial-gradient(circle at 42% 118%, rgba(26, 47, 38, 0.12), transparent 34%),
linear-gradient(120deg, rgba(255, 255, 255, 0.30), transparent 34%, rgba(45, 62, 53, 0.08) 72%, transparent);
opacity: 0.95;
}
#articles .articles-panel::after {
content: “”;
position: absolute;
top: -20%;
left: -30%;
z-index: 0;
width: 65%;
height: 140%;
pointer-events: none;
background: linear-gradient(112deg, transparent 0 36%, rgba(255, 255, 255, 0.52) 44%, rgba(255, 255, 255, 0.18) 48%, transparent 58% 100%);
opacity: 0.62;
transform: rotate(-4deg);
}
#articles .articles-panel > * {
position: relative;
z-index: 1;
}
#articles .articles-head {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin: 0 0 2rem;
padding-bottom: 1.75rem;
border-bottom: 1px solid var(–subtle-rule);
}
#articles .head-content h2 {
margin: 0;
color: var(–text-heading);
font-family: var(–font-serif);
font-size: clamp(2rem, 3.5vw, 2.8rem);
font-weight: 400;
letter-spacing: -0.01em;
line-height: 1.15;
}
#articles .subtitle {
max-width: 780px;
margin: 0.75rem 0 0;
color: var(–text-body);
font-size: clamp(1rem, 1.5vw, 1.08rem);
line-height: 1.65;
}
#articles .article-filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
#articles .article-filter {
appearance: none;
border: 1px solid var(–icon-border);
border-radius: var(–radius-book);
background: transparent;
color: var(–text-heading);
cursor: pointer;
font-family: var(–font-serif);
font-size: 0.95rem;
font-style: italic;
font-weight: 400;
line-height: 1.2;
padding: 0.55rem 1rem;
transition: var(–transition-book);
}
#articles .article-filter:hover,
#articles .article-filter:focus-visible,
#articles .article-filter.is-active {
background: var(–text-heading);
border-color: var(–text-heading);
color: var(–card-bg);
outline: none;
}
#articles .articles-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: clamp(1rem, 2vw, 1.5rem);
align-items: stretch;
}
#articles .article-card {
display: flex;
flex-direction: column;
min-width: 0;
height: 100%;
overflow: hidden;
border-radius: var(–radius-book);
background: var(–card-bg);
border: 1px solid var(–card-border);
box-shadow: var(–shadow-base);
transition: var(–transition-book);
animation: fadeIn 0.4s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
#articles .article-card:hover {
transform: translateY(-4px);
background: var(–card-hover);
box-shadow: var(–shadow-hover);
border-color: var(–text-heading);
}
#articles .article-card.is-hidden { display: none; }
#articles .article-media {
position: relative;
display: block;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
background: #ebe3d5;
border-bottom: 1px solid var(–card-border);
}
#articles .article-media img {
width: 100%;
height: 100%;
object-fit: cover;
filter: sepia(0.1) contrast(1.05);
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
#articles .article-card:hover .article-media img { transform: scale(1.04); }
#articles .article-category-tag {
position: absolute;
top: 0.75rem;
right: 0.75rem;
z-index: 2;
background: rgba(26, 47, 38, 0.85);
color: var(–card-bg);
border: 1px solid rgba(224, 216, 202, 0.2);
border-radius: var(–radius-book);
padding: 4px 12px;
font-size: 0.85rem;
font-style: italic;
font-weight: 400;
line-height: 1.2;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
#articles .article-category-tag.blue,
#articles .article-category-tag.green { color: var(–card-bg); }
#articles .article-content {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
gap: 0.9rem;
padding: clamp(1.25rem, 2.5vw, 1.65rem);
}
#articles .article-meta-top time,
#articles .article-read-time {
color: var(–text-body);
font-size: 0.9rem;
font-style: italic;
}
#articles .article-content h3 {
margin: 0;
color: var(–text-heading);
font-family: var(–font-serif);
font-size: clamp(1.2rem, 2vw, 1.38rem);
font-weight: 400;
line-height: 1.35;
overflow-wrap: anywhere;
}
#articles .article-content h3 a {
color: inherit;
text-decoration: none;
transition: var(–transition-book);
}
#articles .article-content h3 a:hover,
#articles .article-content h3 a:focus-visible {
color: var(–forest-1);
outline: none;
}
#articles .article-excerpt {
margin: 0;
color: var(–text-body);
font-size: 0.98rem;
line-height: 1.65;
overflow-wrap: anywhere;
}
#articles .article-meta-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: auto;
padding-top: 1.15rem;
border-top: 1px solid var(–card-border);
}
#articles .article-read-btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(–text-heading);
border-radius: var(–radius-book);
background: var(–text-heading);
color: var(–card-bg);
font-family: var(–font-serif);
font-size: 0.95rem;
font-weight: 400;
line-height: 1.2;
padding: 0.65rem 1rem;
text-decoration: none;
transition: var(–transition-book);
white-space: nowrap;
}
#articles .article-read-btn:hover,
#articles .article-read-btn:focus-visible {
background: transparent;
color: var(–text-heading);
outline: none;
transform: translateY(-2px);
}
@media (min-width: 1281px) { #articles .articles-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 1280px) { #articles .articles-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 1024px) { #articles .articles-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 860px) {
#articles .articles-head { flex-direction: row; align-items: flex-start; justify-content: space-between; }
#articles .head-content { max-width: 64%; }
#articles .article-filters { justify-content: flex-end; }
}
@media (max-width: 768px), (hover: none) and (pointer: coarse) {
html, body {
position: static !important;
height: auto !important;
margin: 0;
overflow-y: auto !important;
overflow-x: hidden !important;
touch-action: pan-y pinch-zoom !important;
-webkit-overflow-scrolling: touch;
}
body { overflow-x: clip !important; }
#articles { padding: 2rem 0 4rem; }
#articles .container { width: min(100% – 24px, 1400px); }
#articles .articles-panel { padding: 1.25rem; }
}
@media (max-width: 600px) {
#articles .articles-grid { grid-template-columns: 1fr; }
#articles .article-filters { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; }
#articles .article-filter,
#articles .article-read-btn { width: 100%; }
#articles .article-meta-footer { align-items: stretch; flex-direction: column; }
}
(() => {
const wrap = document.getElementById(‘articles’);
if (!wrap) return;
const buttons = wrap.querySelectorAll(‘.article-filters .article-filter’);
const cards = wrap.querySelectorAll(‘.articles-grid .article-card’);
function applyFilter(cat) {
cards.forEach(card => {
// Reset animation
card.style.animation = ‘none’;
card.offsetHeight; /* trigger reflow */
const c = card.getAttribute(‘data-cat’);
const show = (cat === ‘all’) || (c === cat);
card.classList.toggle(‘is-hidden’, !show);
if(show) {
card.style.animation = ‘fadeIn 0.4s ease-out forwards’;
}
});
}
buttons.forEach(btn => {
btn.addEventListener(‘click’, () => {
buttons.forEach(b => {
b.classList.remove(‘is-active’);
b.setAttribute(‘aria-pressed’,’false’);
});
btn.classList.add(‘is-active’);
btn.setAttribute(‘aria-pressed’,’true’);
applyFilter(btn.dataset.filter);
});
});
applyFilter(‘all’);
})();