A globe that shows who's awake
Your people on a globe lit by the real Sun: scrub the next day to see who is awake, or tap once for a time that suits everyone.
React Native Skia · Reanimated · Gesture Handler · Expo UI (SwiftUI) · Expo Haptics · Expo Image
1Demo
2Flow
- 1Spin the globeYoudrag it with a thumb; a flick keeps it turning
- 2Lit by the real SunAppbright day side, city lights at night, each person pinned with their local time
- 3Scrub the next 24 hoursYoudrag the ruler under the needle; a haptic tick each hour
- 4See who is awakeAppthe light sweeps across the dots; a green ring means awake
- 5Find a time for everyoneAppone tap: the dial runs to the middle of the first window when all of you are awake, then a success haptic
3How it fits
4Code
float z = sqrt(1.0 - r2);
float x = d.x;
float y = -d.y;
float y1 = y * cos(lat0) + z * sin(lat0);
float z1 = -y * sin(lat0) + z * cos(lat0);
float lat = asin(clamp(y1, -1.0, 1.0)) / DEG;
float lon = (lon0 + atan(x, z1)) / DEG;
lon = lon - 360.0 * floor((lon + 180.0) / 360.0);
// Snap to the nearest dot: rows every \`spacing\` degrees, each row as many dots as fit around it.
float row = floor(lat / spacing + 0.5);
float latc = row * spacing;
float count = max(1.0, floor(360.0 * cos(latc * DEG) / spacing));
float lonStep = 360.0 / count;
float col = floor((lon + 180.0) / lonStep + 0.5);
float lonc = col * lonStep - 180.0;
float dlat = lat - latc;
float dlon = (lon - lonc) * cos(lat * DEG);
float dist = length(float2(dlat, dlon));
// Antialias across about one pixel, which spans more degrees toward the edge.
float aa = 57.3 / (radius * max(z, 0.12));
float dotRadius = spacing * 0.31;
float inDot = smoothstep(dotRadius + aa * 0.6, dotRadius - aa * 0.6, dist);
float2 m = float2((lonc + 180.0) / 360.0 * maskSize.x, (90.0 - latc) / 180.0 * maskSize.y);
float isLand = land.eval(m).a;
float3 P = toEarth(latc * DEG, lonc * DEG);
float sunDot = dot(P, sun);
float day = smoothstep(-0.06, 0.14, sunDot);
float dusk = smoothstep(0.18, 0.0, abs(sunDot + 0.02));
float3 dayLand = float3(0.93, 0.94, 0.96);
float3 nightLand = float3(0.27, 0.27, 0.3);
float3 col3 = mix(nightLand, dayLand, day);
col3 = mix(col3, float3(1.0, 0.72, 0.5), dusk * 0.35);
// City lights: a scatter of land dots glow warm on the night side.
float city = step(0.86, hash(float2(row, col))) * (1.0 - day);
col3 = mix(col3, float3(1.0, 0.74, 0.38) * 1.25, city);Each pixel snaps to its nearest dot, measured on the sphere, so rows thin toward the poles and edge dots squash on their own.
5Decisions
One SkSL shader for every dot over a Skia circle per dot
each pixel finds its nearest dot on the sphere, so dots foreshorten at the edge and light up one by one with no extra work
The Sun's position from the date and UTC time over a fixed day and night split
the terminator moves as you scrub, and the formula is good to about a degree, finer than the dots
Fixed UTC offsets over a time zone database
the time maths stays a plain addition the UI thread can do every frame (no daylight saving yet)
The middle of the first shared window over its first quarter hour
nobody gets called at the very edge of their day
