mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
52f0fb882f
The screens the app ships itself (welcome, plans, address, own server and the states between) move from the dark card layout to a light design on an animated aerial landscape drawn by a WebGL shader (native-shell/scenery.ts): an island, a coastal town, farmland and mountains in one world. - Welcome: the landscape with the TREK wordmark, three perks in one card, "Explore cloud plans" and "I already have a TREK" as two buttons. - Plans: one tab per plan; choosing one flies the camera to its place, further inland with every plan. Each plan reads as a kind of trip, the price sits in a card with a monthly/yearly switch and counts to its new amount in the store's own format (native-shell/amount.ts), and a yearly price shows what it makes a month. Restore purchases, the renewal terms and the links stay as the store rules require. - Address: the address writes itself on a glass card over the landscape; free, the password rules and the agreement sit on white cards. - Own server: the server address on the glass card; a failed check turns the card red with what went wrong, proxy sign-in and client certificate panels in the same style. - States: server not reachable, certificate needed, already subscribed, setting up (with its steps, then "Arrived") and the slow setup. - iPad, an unfolded phone and a phone on its side get two columns, the landscape beside the content; on a dual-screen device each half takes one screen. Arabic and Hebrew mirror through logical sides only. - The shader caps the pixel ratio at 2, drops to a lower resolution on a slow device, stops in the background and is one still frame with reduced motion. - The sign-in and error page bars over a proxy's page (iOS and Android) take the same look: a white card and a dark button. - Plus Jakarta Sans is bundled for the text, with Montserrat standing in for Cyrillic; the wordmark fonts and the old preview images are gone. - New strings in all 28 languages; the keys of the old screens removed. - The dev sample plans follow the cloud's plans (start, basis, plus, pro).
586 lines
32 KiB
TypeScript
586 lines
32 KiB
TypeScript
/**
|
|
* The landscape behind the first screens of the app: one world seen from above,
|
|
* an island out at sea, the coast with its town, the farmland behind it and the
|
|
* mountains at the back. A fragment shader draws it every frame (water, surf,
|
|
* boats, a car on the road), so it is never a photo and never the same twice.
|
|
*
|
|
* The plan picker moves the camera through that world, one place per plan, so
|
|
* choosing reads as travelling further inland. The scenes are framed for a phone
|
|
* 390 px wide: a wider or taller box (iPad, an unfolded phone) shows more of the
|
|
* world around the same centre.
|
|
*
|
|
* Kept light for the phone it runs on: the device pixel ratio is capped at 2,
|
|
* the picture drops to a lower resolution when frames come slow, the loop stops
|
|
* while the app is in the background or the canvas has left the page, and with
|
|
* reduced motion it is one still frame. Without WebGL the box keeps its plain
|
|
* sea colour from the stylesheet.
|
|
*/
|
|
|
|
/** Where the camera stands for a place, in world pixels, for a 390 px frame. */
|
|
export type Camera = readonly [number, number];
|
|
export const WELCOME_CAMERA: Camera = [0, 0];
|
|
/** One place per plan, in the order the plans come: island, coast, farmland, mountains. */
|
|
const PLAN_CAMERAS: readonly Camera[] = [
|
|
[62, -392],
|
|
[0, 24],
|
|
[60, 290],
|
|
[70, 660],
|
|
];
|
|
export function cameraForPlan(index: number): Camera {
|
|
return PLAN_CAMERAS[Math.max(0, Math.min(PLAN_CAMERAS.length - 1, index))];
|
|
}
|
|
|
|
const FRAME_WIDTH = 390;
|
|
|
|
const FRAGMENT = `
|
|
precision highp float;
|
|
uniform vec2 uRes; uniform float uDpr; uniform float uT; uniform vec2 uCam; uniform float uHaze;
|
|
float coast(float x) { return 128.0 + 12.0 * sin(x * 0.018 + 0.6) + 7.0 * sin(x * 0.051 + 2.1) + 4.0 * sin(x * 0.11); }
|
|
float riverX(float y) { return 168.0 - (y - 120.0) * 0.30 + 9.0 * sin(y * 0.035 + 0.8) + 4.0 * sin(y * 0.09); }
|
|
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
vec2 hash2(vec2 p) { return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453); }
|
|
float noise(vec2 p) { vec2 i = floor(p), f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);
|
|
return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y); }
|
|
float fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += a * noise(p); p = p * 2.03 + vec2(1.7, 9.2); a *= 0.5; } return v; }
|
|
float fbm3(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 3; i++) { v += a * noise(p); p = p * 2.03 + vec2(1.7, 9.2); a *= 0.5; } return v; }
|
|
float wave(vec2 p, float t) {
|
|
return 0.55 * sin(dot(p, vec2(0.040, 0.085)) - t * 1.10) + 0.35 * sin(dot(p, vec2(-0.070, 0.050)) - t * 1.45)
|
|
+ 0.22 * sin(dot(p, vec2(0.110, 0.130)) - t * 2.10) + 0.12 * sin(dot(p, vec2(-0.170, 0.190)) - t * 2.90)
|
|
+ 0.90 * fbm(p * 0.055 + vec2(t * 0.18, t * 0.32));
|
|
}
|
|
float ridged(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { float n = 1.0 - abs(noise(p) * 2.0 - 1.0); v += a * n * n; p = p * 2.07 + vec2(3.1, 1.7); a *= 0.5; } return v; }
|
|
/* Height: gentle near the shore, sharp ridges further inland. */
|
|
float peaks(vec2 p) { float v = 0.0, a = 0.55; for (int i = 0; i < 7; i++) { v += a * (1.0 - abs(noise(p) * 2.0 - 1.0)); p = p * 2.13 + vec2(1.3, 4.7); a *= 0.5; } return v; }
|
|
float ground(vec2 p) {
|
|
float inland = smoothstep(10.0, 170.0, p.y - coast(p.x));
|
|
return fbm(p * 0.011) * 0.4 + ridged(p * 0.014 + 5.0) * 1.15 * inland + fbm3(p * 0.06) * 0.08;
|
|
}
|
|
/* The island out at sea: signed distance to its shore in px (> 0 on land),
|
|
a round main body with an uneven coast and a headland to the east. */
|
|
float islD(vec2 p) {
|
|
vec2 q = (p - vec2(230.0, -250.0)) / vec2(1.5, 1.0);
|
|
float r = length(q);
|
|
float rad = 66.0 + 46.0 * (fbm(q * 0.016 + 4.0) - 0.5) + 30.0 * (fbm(q * 0.045 + 8.0) - 0.5) + 10.0 * (fbm(p * 0.09) - 0.5);
|
|
vec2 q2 = (p - vec2(372.0, -222.0)) / vec2(1.6, 1.0);
|
|
float head = 15.0 + 10.0 * (fbm(p * 0.04 + 9.0) - 0.5) - length(q2);
|
|
return max(rad - r, head);
|
|
}
|
|
float islH(vec2 p) {
|
|
float s = islD(p);
|
|
return smoothstep(0.0, 60.0, s) * (0.5 + 0.7 * fbm(p * 0.018 + 2.0)) + ridged(p * 0.03) * 0.22 * smoothstep(8.0, 50.0, s);
|
|
}
|
|
/* Trees: one per cell at most, a round crown of varying size. Returns the
|
|
crown's cover (0..1) and how far from its centre, for the shading. */
|
|
vec3 tree(vec2 p, float cell) {
|
|
vec2 c = floor(p / cell);
|
|
float best = 9.0; vec2 off = vec2(0.0); float sz = 0.0;
|
|
for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) {
|
|
vec2 g = c + vec2(float(i), float(j));
|
|
vec2 h = hash2(g);
|
|
vec2 o = (g + 0.15 + 0.7 * h) * cell;
|
|
float r = cell * (0.38 + 0.22 * hash(g + 3.1));
|
|
float d = length(p - o) / r;
|
|
if (d < best) { best = d; off = (p - o) / r; sz = r; }
|
|
}
|
|
return vec3(1.0 - smoothstep(0.86, 1.0, best), off);
|
|
}
|
|
/* A Mediterranean town: one house per cell, a rectangle turned the way
|
|
the lane runs there (the direction drifts across town, so lanes curve),
|
|
sized at random and packed close. Whitewashed cubes with a parapet and
|
|
now and then a water tank, pastel walls, terracotta roofs pitched along
|
|
the long side, and the odd blue dome. */
|
|
float town(vec2 p, out vec3 roof, out vec2 hid) {
|
|
float cell = 9.0;
|
|
vec2 c = floor(p / cell);
|
|
roof = vec3(0.0);
|
|
hid = c;
|
|
float got = 0.0;
|
|
for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) {
|
|
vec2 g = c + vec2(float(i), float(j));
|
|
if (hash(g + 2.7) < 0.07) continue;
|
|
vec2 o = (g + 0.5 + 0.35 * (hash2(g) - 0.5)) * cell;
|
|
float ang = (noise(o * 0.018) - 0.5) * 2.4 + (hash(g + 7.7) - 0.5) * 0.25;
|
|
vec2 cs = vec2(cos(ang), sin(ang));
|
|
vec2 rel = p - o;
|
|
vec2 l = vec2(dot(rel, cs), dot(rel, vec2(-cs.y, cs.x)));
|
|
vec2 hs = cell * vec2(0.36 + 0.16 * hash(g + 1.3), 0.27 + 0.14 * hash(g + 4.1));
|
|
if (abs(l.x) < hs.x && abs(l.y) < hs.y) {
|
|
float k = hash(g + 5.3);
|
|
vec3 col = k < 0.44 ? vec3(0.96, 0.95, 0.91) : k < 0.68 ? vec3(0.80, 0.46, 0.32) : k < 0.80 ? vec3(0.93, 0.79, 0.56) : k < 0.90 ? vec3(0.94, 0.75, 0.67) : vec3(0.88, 0.86, 0.80);
|
|
float rim = min(hs.x - abs(l.x), hs.y - abs(l.y));
|
|
if (k >= 0.44 && k < 0.68) {
|
|
// pitched roof: the ridge runs along the long side, one slope lit, one shaded
|
|
float lit = dot(vec2(-cs.y, cs.x), vec2(-0.67, -0.73)) * sign(l.y);
|
|
col *= lit > 0.0 ? 1.08 : 0.80;
|
|
col *= 0.95 + 0.05 * sin(l.x * 3.6);
|
|
col *= 1.0 - 0.25 * (1.0 - smoothstep(0.0, 0.5, abs(l.y)));
|
|
} else {
|
|
// flat roof: a parapet round the edge, a water tank on some
|
|
col *= 1.0 - 0.13 * (1.0 - smoothstep(0.4, 1.1, rim));
|
|
vec2 tk = vec2(hs.x * 0.45, -hs.y * 0.35);
|
|
if (hash(g + 6.2) < 0.35 && length(l - tk) < 0.95) col = vec3(0.45, 0.47, 0.50) + 0.25 * step(0.0, -(l - tk).x - (l - tk).y);
|
|
if (k < 0.44 && hash(g + 4.4) < 0.08 && length(l) < min(hs.x, hs.y) * 0.85) {
|
|
col = mix(vec3(0.12, 0.30, 0.64), vec3(0.58, 0.76, 0.98), clamp(0.55 - dot(rel / (min(hs.x, hs.y) * 0.85), vec2(-0.67, -0.73)) * 0.6, 0.0, 1.0));
|
|
}
|
|
}
|
|
col *= 0.93 + 0.1 * hash(g + 9.9);
|
|
roof = col;
|
|
hid = g;
|
|
got = 1.0;
|
|
}
|
|
}
|
|
return got;
|
|
}
|
|
void main() {
|
|
vec2 sp = vec2(gl_FragCoord.x, uRes.y - gl_FragCoord.y) / uDpr;
|
|
vec2 p = sp + uCam;
|
|
float t = uT;
|
|
vec3 L = normalize(vec3(-0.55, -0.6, 0.62));
|
|
float d = coast(p.x) - p.y; // > 0 at sea, < 0 inland, in px
|
|
float wash = 6.0 + 4.0 * sin(t * 1.2 + p.x * 0.045) + 3.0 * fbm3(vec2(p.x * 0.08, t * 0.6));
|
|
vec3 col;
|
|
if (d > -2.0) {
|
|
/* ── sea ── */
|
|
float e = 0.9, h = wave(p, t);
|
|
vec3 n = normalize(vec3(-(wave(p + vec2(e, 0.0), t) - h) * 1.6, -(wave(p + vec2(0.0, e), t) - h) * 1.6, 1.0));
|
|
float depth = smoothstep(0.0, 120.0, d);
|
|
col = mix(vec3(0.47, 0.83, 0.82), vec3(0.17, 0.58, 0.72), smoothstep(0.0, 0.45, depth));
|
|
col = mix(col, vec3(0.07, 0.33, 0.55), smoothstep(0.45, 1.0, depth));
|
|
col *= 0.78 + 0.32 * clamp(dot(n, -vec3(L.x, L.y, -L.z)), 0.0, 1.0);
|
|
vec3 Ls = normalize(vec3(0.45, -0.55, 0.70));
|
|
vec3 R = reflect(-Ls, n);
|
|
col += vec3(1.0, 0.97, 0.9) * (pow(clamp(R.z, 0.0, 1.0), 90.0) * 0.45 + pow(clamp(R.z, 0.0, 1.0), 900.0) * step(0.55, noise(p * 0.6 + t * 2.0)) * 1.4);
|
|
float ca = pow(1.0 - abs(sin(fbm(p * 0.09 + vec2(t * 0.35, -t * 0.25)) * 9.0)), 6.0);
|
|
col += vec3(0.85, 1.0, 0.95) * ca * 0.22 * (1.0 - smoothstep(0.0, 40.0, d));
|
|
float runs = smoothstep(0.82, 1.0, sin(d * 0.32 + t * 2.2 + fbm3(p * 0.05) * 3.0)) * (1.0 - smoothstep(6.0, 46.0, d));
|
|
float lace = smoothstep(0.45, 0.75, fbm3(p * 0.22 + vec2(t * 0.5, 0.0)));
|
|
float foam = max(1.0 - smoothstep(wash - 4.0, wash + 3.0, d), runs * lace * 0.8);
|
|
col = mix(col, vec3(0.97, 0.99, 1.0), clamp(foam, 0.0, 1.0) * 0.92);
|
|
// boats crossing, each in a lane of its own (46 px apart, wider than any
|
|
// wake), at a speed that swells and eases but never turns back
|
|
for (int k = 0; k < 2; k++) {
|
|
float fk = float(k);
|
|
float dir = fk < 0.5 ? 1.0 : -1.0;
|
|
float speed = 10.0 + fk * 4.0;
|
|
float run = speed * t + 14.0 * sin(t * 0.23 + fk * 2.0) + 6.0 * sin(t * 0.61 + fk * 4.0);
|
|
float lane = 36.0 + fk * 46.0;
|
|
float drift = 2.6 * sin(t * 0.31 + fk * 1.7);
|
|
vec2 bp = vec2(mod(fk * 190.0 + run * dir, 480.0) - 45.0, lane + drift);
|
|
float yaw = 2.6 * 0.31 * cos(t * 0.31 + fk * 1.7) / speed;
|
|
vec2 q = p - bp;
|
|
q = vec2(q.x * dir, q.y);
|
|
q = vec2(q.x + yaw * q.y, q.y - yaw * q.x);
|
|
float back = -q.x;
|
|
float open = 0.29 + 0.05 * sin(t * 1.3 + fk * 3.0 + back * 0.05);
|
|
float spread = back * open;
|
|
float churn = smoothstep(0.25, 0.65, noise(vec2(back * 0.35 - t * 1.6, q.y * 0.45 + fk * 7.0)));
|
|
float edgeW = 0.7 + 0.8 * noise(vec2(back * 0.2 + fk * 3.0, t * 0.8));
|
|
float fade = step(0.0, back) * (1.0 - smoothstep(8.0, 64.0, back));
|
|
float wake = (1.0 - smoothstep(edgeW * 0.6, edgeW * 1.8, abs(abs(q.y) - spread))) * fade * (0.55 + 0.45 * churn);
|
|
wake += (1.0 - smoothstep(0.0, 2.4, abs(q.y))) * step(0.0, back) * (1.0 - smoothstep(4.0, 30.0, back)) * 0.7 * churn;
|
|
col = mix(col, vec3(0.96, 0.98, 1.0), clamp(wake, 0.0, 1.0) * 0.78);
|
|
float hull = length(q / vec2(6.5, 2.4));
|
|
col = mix(col, vec3(0.20, 0.30, 0.40), (1.0 - smoothstep(1.0, 1.5, length((q - vec2(-1.0, 1.4)) / vec2(6.5, 2.4)))) * 0.25);
|
|
col = mix(col, mix(vec3(0.98), vec3(0.80, 0.82, 0.85), step(0.0, q.y)), 1.0 - smoothstep(0.92, 1.0, hull));
|
|
col = mix(col, vec3(0.55, 0.36, 0.24), 1.0 - smoothstep(0.9, 1.0, length((q - vec2(-1.0, 0.0)) / vec2(2.4, 1.2))));
|
|
}
|
|
// the island: a shelf of shallow water, a reef, surf, a beach, a wooded hill
|
|
if (abs(p.y + 250.0) < 175.0 && abs(p.x - 265.0) < 310.0) {
|
|
float sI = islD(p);
|
|
if (sI < 0.0) {
|
|
float sd = -sI;
|
|
float shelf = 1.0 - smoothstep(4.0, 78.0, sd);
|
|
float ripple = 0.5 + 0.5 * sin(dot(p, vec2(0.42, 0.25)) + fbm(p * 0.05) * 6.0);
|
|
vec3 sh = mix(vec3(0.13, 0.52, 0.66), vec3(0.40, 0.83, 0.81), smoothstep(62.0, 10.0, sd));
|
|
sh = mix(sh, vec3(0.80, 0.86, 0.74), smoothstep(26.0, 0.0, sd) * 0.4);
|
|
sh *= 0.95 + 0.08 * ripple * smoothstep(50.0, 4.0, sd);
|
|
float swell = sin(sd * 0.3 + t * 1.8 + fbm3(p * 0.03) * 4.0);
|
|
sh *= 1.0 + 0.09 * swell * smoothstep(70.0, 20.0, sd) * smoothstep(0.0, 8.0, sd);
|
|
float reef = smoothstep(0.57, 0.68, fbm(p * 0.045 + 13.0)) * smoothstep(12.0, 22.0, sd) * (1.0 - smoothstep(46.0, 60.0, sd));
|
|
sh = mix(sh, vec3(0.18, 0.40, 0.42) * (0.82 + 0.3 * noise(p * 0.5)), reef * 0.75);
|
|
float lw = 0.85 + 0.3 * clamp(dot(n, -vec3(L.x, L.y, -L.z)), 0.0, 1.0);
|
|
col = mix(col, sh * lw, shelf);
|
|
float cs = pow(1.0 - abs(sin(fbm(p * 0.12 + vec2(t * 0.4, -t * 0.3)) * 10.0)), 7.0);
|
|
col += vec3(0.9, 1.0, 0.95) * cs * 0.24 * smoothstep(45.0, 4.0, sd);
|
|
// surf breaking along the reef edge, in pieces
|
|
float reefLine = 44.0 + 12.0 * (fbm(p * 0.02 + 5.0) - 0.5);
|
|
float brk = (1.0 - smoothstep(0.0, 2.0 + 2.5 * fbm3(p * 0.04 + vec2(t * 0.25, 0.0)), abs(sd - reefLine - 2.5 * sin(t * 0.8 + p.x * 0.03))));
|
|
brk *= smoothstep(0.35, 0.62, noise(p * 0.08 + vec2(t * 0.6, 0.0))) * smoothstep(0.42, 0.56, fbm(p * 0.03 + 13.0));
|
|
col = mix(col, vec3(0.96, 0.99, 1.0), brk * 0.6);
|
|
// waves rolling in, foam lines thinning out as they come, the swash on the sand
|
|
float lace2 = smoothstep(0.42, 0.72, fbm3(p * 0.25 + vec2(t * 0.4, 0.0)));
|
|
float roll = smoothstep(0.9, 1.0, swell) * (1.0 - smoothstep(5.0, 26.0, sd));
|
|
float iw = 4.5 + 3.5 * sin(t * 1.1 + fbm(p * 0.03) * 6.0);
|
|
float swash = 1.0 - smoothstep(iw - 3.0, iw + 2.5, sd);
|
|
col = mix(col, vec3(0.97, 0.99, 1.0), clamp(max(swash * (0.55 + 0.45 * lace2), roll * lace2 * 0.8), 0.0, 1.0) * 0.9);
|
|
} else {
|
|
float hI = islH(p);
|
|
vec3 nI = normalize(vec3(-(islH(p + vec2(1.0, 0.0)) - hI) * 110.0, -(islH(p + vec2(0.0, 1.0)) - hI) * 110.0, 1.0));
|
|
float shI = clamp(dot(nI, normalize(vec3(-L.x, -L.y, L.z))), 0.0, 1.0);
|
|
float bwI = 5.0 + 15.0 * fbm(p * 0.03 + 2.0);
|
|
vec3 sandI = vec3(0.95, 0.89, 0.74) + (hash(floor(p * 3.0)) - 0.5) * 0.05;
|
|
sandI = mix(sandI, vec3(0.76, 0.69, 0.56), (1.0 - smoothstep(0.0, 4.5, sI)) * 0.75);
|
|
float big = step(0.55, fbm(p * 0.025 + 6.0));
|
|
vec3 tr = big > 0.5 ? tree(p, 8.0) : tree(p, 4.6);
|
|
vec3 trS = big > 0.5 ? tree(p - vec2(2.6, 3.0), 8.0) : tree(p - vec2(1.7, 2.0), 4.6);
|
|
float lit = clamp(0.62 - dot(tr.yz, vec2(L.x, L.y)) * 0.55, 0.0, 1.0);
|
|
float jung = smoothstep(bwI, bwI + 8.0, sI);
|
|
vec3 under = mix(vec3(0.46, 0.55, 0.28), vec3(0.28, 0.42, 0.19), noise(p * 0.1)) * (1.0 - 0.35 * trS.x);
|
|
vec3 canopy = mix(vec3(0.09, 0.27, 0.12), vec3(0.42, 0.62, 0.28), lit) * (0.9 + 0.2 * noise(p * 2.0));
|
|
vec3 landI = mix(sandI, under, jung);
|
|
landI *= 1.0 - 0.3 * trS.x * smoothstep(bwI - 4.0, bwI, sI) * (1.0 - jung);
|
|
landI = mix(landI, canopy, tr.x * smoothstep(bwI - 3.0, bwI + 5.0, sI) * (0.35 + 0.65 * smoothstep(0.22, 0.36, fbm(p * 0.035 + 1.0))));
|
|
// bare rock on the steep top and boulders where the headland meets the sea
|
|
float slI = 1.0 - nI.z;
|
|
landI = mix(landI, vec3(0.62, 0.58, 0.50) * (0.9 + 0.15 * noise(p * 0.8)), smoothstep(0.7, 0.88, slI) * 0.6);
|
|
vec3 bo = tree(p, 4.0);
|
|
float blit = clamp(0.6 - dot(bo.yz, vec2(L.x, L.y)) * 0.6, 0.0, 1.0);
|
|
vec3 rocks = mix(vec3(0.30, 0.29, 0.27), mix(vec3(0.52, 0.50, 0.46), vec3(0.80, 0.77, 0.71), blit), bo.x);
|
|
float rk = smoothstep(0.6, 0.7, fbm(p * 0.05 + 4.0)) * (1.0 - smoothstep(bwI - 2.0, bwI + 4.0, sI));
|
|
landI = mix(landI, rocks, rk);
|
|
landI *= 0.5 + 0.7 * shI;
|
|
col = mix(vec3(0.90, 0.95, 0.93), landI, smoothstep(0.0, 1.4, sI));
|
|
}
|
|
// a sailboat going round, well outside the reef
|
|
float sa = t * 0.06;
|
|
vec2 sb = vec2(230.0, -250.0) + vec2(cos(sa) * 1.5, sin(sa)) * 140.0;
|
|
vec2 tang = normalize(vec2(-sin(sa) * 1.5, cos(sa)));
|
|
vec2 q = p - sb;
|
|
q = vec2(dot(q, tang), dot(q, vec2(-tang.y, tang.x)));
|
|
float back = -q.x;
|
|
float wk = (1.0 - smoothstep(0.5, 1.6, abs(abs(q.y) - back * 0.22))) * step(0.0, back) * (1.0 - smoothstep(6.0, 44.0, back));
|
|
col = mix(col, vec3(0.96, 0.98, 1.0), wk * 0.7);
|
|
col = mix(col, vec3(0.10, 0.22, 0.32), (1.0 - smoothstep(1.0, 1.4, length((q - vec2(-1.5, 2.6)) / vec2(5.0, 3.0)))) * 0.25);
|
|
col = mix(col, vec3(0.98), 1.0 - smoothstep(0.9, 1.0, length(q / vec2(4.8, 1.7))));
|
|
float sail = max(abs(q.x + 0.4) - 2.6, abs(q.y + 1.2 - (q.x + 0.4) * 0.15) - 0.7);
|
|
col = mix(col, vec3(0.88, 0.86, 0.82), 1.0 - smoothstep(-0.2, 0.3, sail));
|
|
}
|
|
} else {
|
|
/* ── land: hillside, macchia, and the town down at the water ── */
|
|
float e = 1.2, h = ground(p);
|
|
vec3 n = normalize(vec3(-(ground(p + vec2(e, 0.0)) - h) * 17.0, -(ground(p + vec2(0.0, e)) - h) * 17.0, 1.0));
|
|
float shade = clamp(dot(n, normalize(vec3(-L.x, -L.y, L.z))), 0.0, 1.0);
|
|
float slope = 1.0 - n.z;
|
|
float veg = fbm(p * 0.03 + 2.0);
|
|
vec3 base = mix(vec3(0.78, 0.72, 0.53), vec3(0.50, 0.56, 0.33), smoothstep(0.42, 0.58, veg));
|
|
base = mix(base, vec3(0.36, 0.45, 0.25), smoothstep(0.6, 0.7, veg) * 0.8);
|
|
base *= 0.9 + 0.16 * noise(p * 1.4) + 0.06 * (hash(floor(p * 2.0)) - 0.5);
|
|
float shrub = smoothstep(0.42, 0.6, fbm(p * 0.022 + 11.0));
|
|
vec3 shT = tree(p - vec2(2.0, 2.4), 6.0);
|
|
base *= 1.0 - 0.34 * shT.x * shrub;
|
|
vec3 tr = tree(p, 6.0);
|
|
float lit = clamp(0.62 - dot(tr.yz, vec2(L.x, L.y)) * 0.55, 0.0, 1.0);
|
|
vec3 leaf = mix(vec3(0.16, 0.27, 0.14), vec3(0.45, 0.55, 0.30), lit) * (0.9 + 0.2 * noise(p * 2.3));
|
|
base = mix(base, leaf, tr.x * shrub * (1.0 - smoothstep(0.35, 0.6, slope)));
|
|
vec3 limestone = mix(vec3(0.70, 0.66, 0.58), vec3(0.88, 0.85, 0.78), noise(p * 0.7)) * (0.92 + 0.12 * noise(p * 3.1));
|
|
base = mix(base, limestone, smoothstep(0.3, 0.6, slope) * 0.75);
|
|
// the town: dense by the water, thinning out up the hill, house by house
|
|
vec3 roof, tmpRoof; vec2 hid, tmpId;
|
|
float occ = town(p, roof, hid);
|
|
float occS = town(p - vec2(2.4, 3.0), tmpRoof, tmpId);
|
|
float near = exp(d / 150.0);
|
|
float want = fbm(p * 0.011 + 3.0) * 0.75 * exp(d / 420.0) + near * 0.6;
|
|
float here = step(0.55 + 0.12 * hash(hid + 6.6), want);
|
|
float hereS = step(0.55 + 0.12 * hash(tmpId + 6.6), want);
|
|
// lanes are paved only in the dense heart; where no house stands the hillside shows
|
|
float dense = smoothstep(0.62, 0.7, want);
|
|
vec3 stone = vec3(0.87, 0.84, 0.77) * (0.95 + 0.07 * noise(p * 1.6));
|
|
vec3 open = roof.x < -0.5 ? mix(stone, vec3(0.48, 0.57, 0.34), step(0.5, hash(hid + 3.0))) : stone;
|
|
col = mix(base, open, dense);
|
|
if (occ * here > 0.5) col = roof;
|
|
else if (occS * hereS > 0.5) col *= 0.64;
|
|
// the hinterland: parcels with their own crop and row direction, hedges between
|
|
float farm = smoothstep(150.0, 200.0, -d) * (1.0 - smoothstep(450.0, 510.0, -d));
|
|
if (farm > 0.0) {
|
|
vec2 fp = (p + vec2(fbm(p * 0.015), fbm(p * 0.015 + 7.0)) * 10.0) * vec2(0.62, 1.0);
|
|
vec2 fc = floor(fp / 58.0);
|
|
float b1 = 99.0, b2 = 99.0; vec2 fid = vec2(0.0);
|
|
for (int fj = -1; fj <= 1; fj++) for (int fi = -1; fi <= 1; fi++) {
|
|
vec2 g = fc + vec2(float(fi), float(fj));
|
|
float dd = length(fp - (g + 0.25 + 0.5 * hash2(g)) * 58.0);
|
|
if (dd < b1) { b2 = b1; b1 = dd; fid = g; } else if (dd < b2) { b2 = dd; }
|
|
}
|
|
float kind = hash(fid + 1.7);
|
|
float ang = hash(fid + 4.2) * 3.1416;
|
|
vec2 dir = vec2(cos(ang), sin(ang));
|
|
float u = dot(p, dir), v = dot(p, vec2(-dir.y, dir.x));
|
|
vec3 parcel;
|
|
if (kind < 0.36) {
|
|
float r = smoothstep(-0.2, 0.6, sin(u * 6.2831 / 4.0));
|
|
vec3 vine = mix(vec3(0.24, 0.36, 0.16), vec3(0.44, 0.54, 0.25), noise(p * 1.4)) * (0.85 + 0.2 * noise(vec2(u * 0.3, v * 1.6)));
|
|
parcel = mix(vec3(0.67, 0.57, 0.42), vine, r);
|
|
} else if (kind < 0.56) {
|
|
parcel = mix(vec3(0.88, 0.78, 0.52), vec3(0.80, 0.68, 0.43), 0.5 + 0.5 * sin(u * 6.2831 / 2.4)) * (0.94 + 0.1 * noise(p * 0.5));
|
|
} else if (kind < 0.74) {
|
|
vec2 q = vec2(u, v) / 9.0;
|
|
vec2 qc = fract(q) - 0.5 + (hash2(floor(q)) - 0.5) * 0.18;
|
|
float olive = 1.0 - smoothstep(0.2, 0.33, length(qc));
|
|
float oshadow = 1.0 - smoothstep(0.2, 0.36, length(qc - vec2(0.12, 0.16)));
|
|
vec3 soil = vec3(0.76, 0.68, 0.52) * (1.0 - 0.28 * oshadow);
|
|
parcel = mix(soil, mix(vec3(0.36, 0.42, 0.28), vec3(0.56, 0.60, 0.42), clamp(0.6 - dot(qc, vec2(L.x, L.y)) * 1.4, 0.0, 1.0)), olive);
|
|
} else if (kind < 0.88) {
|
|
parcel = mix(vec3(0.50, 0.39, 0.28), vec3(0.63, 0.51, 0.37), 0.5 + 0.5 * sin(u * 6.2831 / 1.9)) * (0.95 + 0.08 * noise(p * 0.6));
|
|
} else {
|
|
parcel = mix(vec3(0.47, 0.57, 0.29), vec3(0.58, 0.63, 0.33), noise(p * 0.08));
|
|
}
|
|
// hedgerows and the odd cypress along the field edges
|
|
float edgeD = (b2 - b1) * 0.5;
|
|
parcel *= 0.92 + 0.16 * hash(fid + 5.5);
|
|
float hedge = 1.0 - smoothstep(0.5, 1.5, edgeD);
|
|
vec3 cy = tree(p, 5.0);
|
|
parcel = mix(parcel, vec3(0.30, 0.35, 0.20), hedge * 0.75);
|
|
parcel = mix(parcel, vec3(0.12, 0.20, 0.10), cy.x * (1.0 - smoothstep(1.5, 3.5, edgeD)) * step(0.7, hash(fid + 9.0)));
|
|
float level = 1.0 - smoothstep(0.38, 0.6, slope);
|
|
col = mix(col, parcel, farm * level * (1.0 - step(0.5, occ * here)));
|
|
// a country road winding through, a car on it
|
|
float ry = 425.0 + 26.0 * sin(p.x * 0.011 + 1.0) + 9.0 * sin(p.x * 0.029);
|
|
float rd = abs(p.y - ry);
|
|
col = mix(col, vec3(0.86, 0.82, 0.72), (1.0 - smoothstep(4.6, 5.6, rd)) * farm);
|
|
col = mix(col, vec3(0.36, 0.36, 0.37) * (0.95 + 0.08 * noise(p * 2.0)), (1.0 - smoothstep(3.4, 4.2, rd)) * farm);
|
|
col = mix(col, vec3(0.95), (1.0 - smoothstep(0.25, 0.6, rd)) * step(0.5, fract(p.x / 9.0)) * farm);
|
|
float cx = mod(t * 22.0 + 6.0 * sin(t * 0.4), 760.0) - 120.0;
|
|
float cyy = 425.0 + 26.0 * sin(cx * 0.011 + 1.0) + 9.0 * sin(cx * 0.029) - 1.6;
|
|
float slopeR = 26.0 * 0.011 * cos(cx * 0.011 + 1.0) + 9.0 * 0.029 * cos(cx * 0.029);
|
|
vec2 cq = p - vec2(cx, cyy);
|
|
float ca = atan(slopeR);
|
|
cq = vec2(cq.x * cos(ca) + cq.y * sin(ca), -cq.x * sin(ca) + cq.y * cos(ca));
|
|
float body = max(abs(cq.x) - 3.4, abs(cq.y) - 1.4);
|
|
col = mix(col, vec3(0.14, 0.14, 0.15), (1.0 - smoothstep(0.0, 1.4, max(abs(cq.x - 0.8) - 3.6, abs(cq.y - 0.9) - 1.6))) * 0.35);
|
|
col = mix(col, vec3(0.78, 0.16, 0.13), 1.0 - smoothstep(-0.2, 0.4, body));
|
|
col = mix(col, vec3(0.20, 0.24, 0.30), (1.0 - smoothstep(-0.2, 0.3, max(abs(cq.x - 0.9) - 1.0, abs(cq.y) - 1.0))));
|
|
}
|
|
col *= 0.64 + 0.5 * shade;
|
|
col *= 1.0 - 0.1 * smoothstep(0.55, 0.0, h);
|
|
// the high country: broad massifs, conifers in the valley, snow on the tops
|
|
float alpine = smoothstep(470.0, 590.0, -d);
|
|
if (alpine > 0.0) {
|
|
float lakeD = length((p - vec2(250.0, 790.0)) / vec2(2.3, 1.0));
|
|
float valley = mix(0.35, 1.0, smoothstep(26.0, 80.0, lakeD));
|
|
float hA = (peaks(p * 0.006 + 5.0) * 1.3 - 0.2 + fbm(p * 0.035) * 0.05) * valley;
|
|
float hX = (peaks((p + vec2(1.5, 0.0)) * 0.006 + 5.0) * 1.3 - 0.2 + fbm((p + vec2(1.5, 0.0)) * 0.035) * 0.05) * valley;
|
|
float hY = (peaks((p + vec2(0.0, 1.5)) * 0.006 + 5.0) * 1.3 - 0.2 + fbm((p + vec2(0.0, 1.5)) * 0.035) * 0.05) * valley;
|
|
vec3 nA = normalize(vec3(-(hX - hA) * 95.0, -(hY - hA) * 95.0, 1.0));
|
|
float shA = clamp(dot(nA, normalize(vec3(-L.x, -L.y, L.z))), 0.0, 1.0);
|
|
float slA = 1.0 - nA.z;
|
|
vec3 rockA = mix(vec3(0.46, 0.43, 0.39), vec3(0.62, 0.58, 0.52), noise(p * 0.05)) * (0.94 + 0.08 * noise(p * 0.9));
|
|
vec3 trA = tree(p, 4.4);
|
|
vec3 trS = tree(p - vec2(1.6, 2.0), 4.4);
|
|
float conifer = (1.0 - smoothstep(0.40, 0.50, hA + (noise(p * 0.06) - 0.5) * 0.08)) * (1.0 - smoothstep(0.5, 0.75, slA));
|
|
vec3 meadow = mix(vec3(0.42, 0.50, 0.28), vec3(0.52, 0.56, 0.32), noise(p * 0.1));
|
|
vec3 forestA = meadow * (1.0 - 0.4 * trS.x);
|
|
forestA = mix(forestA, mix(vec3(0.07, 0.15, 0.09), vec3(0.20, 0.33, 0.18), clamp(0.6 - dot(trA.yz, vec2(L.x, L.y)) * 0.6, 0.0, 1.0)), trA.x * smoothstep(0.2, 0.5, fbm(p * 0.02 + 3.0) + 0.25));
|
|
vec3 alp = mix(rockA, forestA, conifer);
|
|
float snowA = smoothstep(0.75, 0.81, hA + (noise(p * 0.09) - 0.5) * 0.1) * (1.0 - smoothstep(0.55, 0.8, slA));
|
|
snowA = max(snowA, smoothstep(0.86, 0.94, hA));
|
|
alp *= 0.36 + 0.86 * shA;
|
|
alp = mix(alp * vec3(0.8, 0.87, 1.05), alp * vec3(1.04, 1.0, 0.95), smoothstep(0.35, 0.8, shA));
|
|
vec3 snowC = mix(vec3(0.70, 0.77, 0.88), vec3(0.98, 0.98, 0.97), smoothstep(0.2, 0.75, shA)) * (0.96 + 0.05 * noise(p * 0.6));
|
|
alp = mix(alp, snowC, snowA);
|
|
col = mix(col, alp, alpine);
|
|
}
|
|
// a mountain lake in a valley
|
|
vec2 lc = vec2(250.0, 790.0);
|
|
float ld = length((p - lc) / vec2(2.3, 1.0)) + (fbm(p * 0.05) - 0.5) * 14.0;
|
|
if (ld < 34.0) {
|
|
float lw = wave(p * 1.3, t * 0.6);
|
|
vec3 lake = mix(vec3(0.12, 0.36, 0.48), vec3(0.24, 0.55, 0.62), smoothstep(34.0, 12.0, ld));
|
|
lake *= 0.86 + 0.12 * lw;
|
|
lake += vec3(1.0, 0.98, 0.92) * pow(noise(p * 0.6 + t * 1.2), 14.0) * 0.7;
|
|
col = mix(col, lake, smoothstep(34.0, 31.0, ld));
|
|
col = mix(col, vec3(0.82, 0.80, 0.74), (1.0 - smoothstep(0.0, 1.8, abs(ld - 32.0))) * 0.8);
|
|
}
|
|
// the shore: small coves of sand between rocks, wet where the wash runs
|
|
float bw = 17.0 + 10.0 * fbm3(vec2(p.x * 0.03, 1.0));
|
|
float shore = 1.0 - smoothstep(bw - 3.0, bw + 2.0, -d);
|
|
float rock = smoothstep(0.56, 0.66, fbm(p * 0.05 + 4.0));
|
|
vec3 sand = vec3(0.93, 0.86, 0.71) + (hash(floor(p * 3.0)) - 0.5) * 0.06;
|
|
float wet = 1.0 - smoothstep(0.0, 6.0 + wash * 0.4, -d);
|
|
sand = mix(sand, vec3(0.74, 0.67, 0.55), wet * 0.8);
|
|
// boulders: round stones lit from the top left, darker in the cracks between
|
|
vec3 bo = tree(p, 4.5);
|
|
float blit = clamp(0.6 - dot(bo.yz, vec2(L.x, L.y)) * 0.6, 0.0, 1.0);
|
|
vec3 rocks = mix(vec3(0.30, 0.29, 0.27), mix(vec3(0.52, 0.50, 0.46), vec3(0.80, 0.77, 0.71), blit), bo.x);
|
|
vec3 edgeCol = mix(sand, rocks, rock);
|
|
col = mix(col, edgeCol, shore);
|
|
}
|
|
// haze into the page below: a long, uneven edge that drifts slowly
|
|
float H = uRes.y / uDpr;
|
|
float edge = H - 118.0 + 42.0 * (fbm3(vec2(sp.x * 0.011, t * 0.04)) - 0.5) * 2.0 + 22.0 * (fbm(sp * 0.028 + vec2(t * 0.05, 0.0)) - 0.5) * 2.0;
|
|
float mist = smoothstep(edge - 34.0, edge + 84.0, sp.y);
|
|
mist = clamp(mist + smoothstep(H - 24.0, H, sp.y), 0.0, 1.0);
|
|
col = mix(col, vec3(1.0, 0.98, 0.961), mist * uHaze);
|
|
gl_FragColor = vec4(col, 1.0);
|
|
}`;
|
|
|
|
const VERTEX = 'attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }';
|
|
|
|
export interface Scenery {
|
|
/** Glides the camera to another place; a still picture jumps there. */
|
|
flyTo(camera: Camera): void;
|
|
}
|
|
|
|
interface SceneryOptions {
|
|
/** The height the scene is framed for; the box may be taller or shorter. */
|
|
frame: number;
|
|
camera?: Camera;
|
|
/** The soft fade into the page at the foot of the picture; off beside a column. */
|
|
haze?: boolean;
|
|
}
|
|
|
|
const reducedMotion = (): boolean =>
|
|
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
export function startScenery(box: HTMLElement, opts: SceneryOptions): Scenery | null {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.className = 'scenery';
|
|
canvas.setAttribute('aria-hidden', 'true');
|
|
box.prepend(canvas);
|
|
const gl = canvas.getContext('webgl', { antialias: false, premultipliedAlpha: false, powerPreference: 'low-power' });
|
|
if (!gl) {
|
|
canvas.remove();
|
|
return null;
|
|
}
|
|
const compile = (type: number, src: string): WebGLShader | null => {
|
|
const shader = gl.createShader(type);
|
|
if (!shader) return null;
|
|
gl.shaderSource(shader, src);
|
|
gl.compileShader(shader);
|
|
return shader;
|
|
};
|
|
const program = gl.createProgram();
|
|
const vs = compile(gl.VERTEX_SHADER, VERTEX);
|
|
const fs = compile(gl.FRAGMENT_SHADER, FRAGMENT);
|
|
if (!program || !vs || !fs) {
|
|
canvas.remove();
|
|
return null;
|
|
}
|
|
gl.attachShader(program, vs);
|
|
gl.attachShader(program, fs);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
canvas.remove();
|
|
return null;
|
|
}
|
|
gl.useProgram(program);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
|
const position = gl.getAttribLocation(program, 'a');
|
|
gl.enableVertexAttribArray(position);
|
|
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
|
const uRes = gl.getUniformLocation(program, 'uRes');
|
|
const uDpr = gl.getUniformLocation(program, 'uDpr');
|
|
const uT = gl.getUniformLocation(program, 'uT');
|
|
const uCam = gl.getUniformLocation(program, 'uCam');
|
|
gl.uniform1f(gl.getUniformLocation(program, 'uHaze'), opts.haze === false ? 0 : 1);
|
|
|
|
// The picture's resolution: the screen's, capped at 2, scaled down when frames come slow.
|
|
let quality = 1;
|
|
let offset: [number, number] = [0, 0];
|
|
const resize = () => {
|
|
const cssWidth = box.clientWidth || FRAME_WIDTH;
|
|
const cssHeight = box.clientHeight || opts.frame;
|
|
const ratio = Math.min(2, Math.max(1, window.devicePixelRatio || 1)) * quality;
|
|
canvas.width = Math.max(1, Math.round(cssWidth * ratio));
|
|
canvas.height = Math.max(1, Math.round(cssHeight * ratio));
|
|
canvas.style.height = `${cssHeight}px`;
|
|
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
gl.uniform2f(uRes, canvas.width, canvas.height);
|
|
gl.uniform1f(uDpr, canvas.width / cssWidth);
|
|
// The scene is framed for a phone: a bigger box shows more around the same
|
|
// centre, a smaller one (a small iPhone) the middle of it.
|
|
offset = [(cssWidth - FRAME_WIDTH) / 2, (cssHeight - opts.frame) / 2];
|
|
};
|
|
resize();
|
|
|
|
const still = reducedMotion();
|
|
const born = performance.now();
|
|
let camera: [number, number] = [opts.camera?.[0] ?? 0, opts.camera?.[1] ?? 0];
|
|
let flight: { from: [number, number]; to: Camera; start: number; duration: number } | null = null;
|
|
let running = false;
|
|
let lastFrame = 0;
|
|
let measured = 0;
|
|
let slowFrames = 0;
|
|
|
|
const draw = (now: number) => {
|
|
if (flight) {
|
|
const k = Math.min(1, (now - flight.start) / flight.duration);
|
|
const e = k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2;
|
|
camera = [
|
|
flight.from[0] + (flight.to[0] - flight.from[0]) * e,
|
|
flight.from[1] + (flight.to[1] - flight.from[1]) * e,
|
|
];
|
|
if (k >= 1) flight = null;
|
|
}
|
|
gl.uniform2f(uCam, camera[0] - offset[0], camera[1] - offset[1]);
|
|
gl.uniform1f(uT, still ? 4 : (now - born) / 1000 + 4);
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
};
|
|
|
|
const frame = (now: number) => {
|
|
if (!canvas.isConnected || document.hidden) {
|
|
running = false;
|
|
return;
|
|
}
|
|
// A device that cannot keep up gets a smaller picture once, rather than a stutter.
|
|
if (lastFrame && quality === 1 && measured < 90) {
|
|
measured += 1;
|
|
if (now - lastFrame > 24) slowFrames += 1;
|
|
if (measured === 90 && slowFrames > 45) {
|
|
quality = 0.7;
|
|
resize();
|
|
}
|
|
}
|
|
lastFrame = now;
|
|
draw(now);
|
|
if (!still || flight) requestAnimationFrame(frame);
|
|
else running = false;
|
|
};
|
|
const run = () => {
|
|
if (running) return;
|
|
running = true;
|
|
lastFrame = 0;
|
|
requestAnimationFrame(frame);
|
|
};
|
|
run();
|
|
|
|
// The loop stops in the background (document.hidden) and starts again on return.
|
|
const wake = () => {
|
|
if (!document.hidden && canvas.isConnected) run();
|
|
};
|
|
document.addEventListener('visibilitychange', wake);
|
|
if (typeof ResizeObserver === 'function') {
|
|
const observer = new ResizeObserver(() => {
|
|
if (!canvas.isConnected) {
|
|
observer.disconnect();
|
|
document.removeEventListener('visibilitychange', wake);
|
|
return;
|
|
}
|
|
resize();
|
|
if (still) run();
|
|
});
|
|
observer.observe(box);
|
|
}
|
|
canvas.addEventListener('webglcontextlost', (event) => {
|
|
event.preventDefault();
|
|
canvas.remove();
|
|
});
|
|
|
|
return {
|
|
flyTo(to: Camera) {
|
|
if (still) {
|
|
camera = [to[0], to[1]];
|
|
run();
|
|
return;
|
|
}
|
|
const distance = Math.hypot(to[0] - camera[0], to[1] - camera[1]);
|
|
flight = {
|
|
from: [camera[0], camera[1]],
|
|
to,
|
|
start: performance.now(),
|
|
duration: Math.min(1700, 700 + distance * 1.1),
|
|
};
|
|
run();
|
|
},
|
|
};
|
|
}
|