Skip to content

Add World tour #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,428 changes: 3,257 additions & 3,171 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"@sveltejs/package": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@types/d3-geo": "^3.0.3",
"@types/topojson-client": "^3.1.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
Expand All @@ -46,6 +48,8 @@
"types": "./dist/index.d.ts",
"type": "module",
"dependencies": {
"chart.js": "^4.2.1"
"chart.js": "^4.2.1",
"d3-geo": "^3.1.0",
"topojson-client": "^3.1.0"
}
}
120 changes: 120 additions & 0 deletions src/lib/Projection/Projection.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script lang="ts">
import {
geoPath,
geoOrthographic,
geoCentroid,
geoInterpolate,
type GeoPermissibleObjects,
type ExtendedFeatureCollection
} from 'd3-geo';
import { tween } from './tween';
import { feature } from 'topojson-client';
import { Versor } from './Versor';

export let width = 720;
export let tilt = 20;
export let name = ''; //name of country
export let world: TopoJSON.Topology;

const sphere: GeoPermissibleObjects = { type: 'Sphere' };

let land: GeoPermissibleObjects | null;
let countryGraphic: GeoPermissibleObjects | null = null;
let arc: GeoPermissibleObjects | null = null;
let projection = geoOrthographic();
let position1: [number, number] = [0, 0];
let position2: [number, number] = [0, 0];
let r1: [number, number, number] = [0, 0, 0];
let r2: [number, number, number] = [0, 0, 0];
let interpolatePosition: (t: number) => [number, number];
let interpolateVersor: (t: number) => [number, number, number];
let inAnimation: Promise<void> | null = null;
let outAnimation: Promise<void> | null = null;

function getHeight(width: number) {
const [[x0, y0], [x1, y1]] = geoPath(projection.fitWidth(width, sphere)).bounds(sphere);
const dy = Math.ceil(y1 - y0);
const l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale((projection.scale() * (l - 1)) / l).precision(0.2);
projection = projection;
return dy;
}

function getCountries(world: TopoJSON.Topology) {
if (!world) return [];

const collection = feature(world, world.objects.countries) as ExtendedFeatureCollection;

if (collection.features) {
return collection.features;
}

return [];
}

async function worldTour(countryName: string) {
//see if an animation is currently running
if (inAnimation) await inAnimation;
if (outAnimation) await outAnimation;
if (countryName != name) return; //Cancel old animations if people switch before it can complete

const country = countries.find((country) => country?.properties?.name === countryName);

if (!country) return;

countryGraphic = country;

(position1 = position2), (position2 = geoCentroid(countryGraphic));
(r1 = r2), (r2 = [-position2[0], tilt - position2[1], 0]);

interpolatePosition = geoInterpolate(position1, position2);
interpolateVersor = Versor.interpolateAngles(r1, r2);

inAnimation = tween.set(1);
requestAnimationFrame(inAnimationFunc);
await inAnimation;
inAnimation = null;

outAnimation = tween.set(0);
interpolatePosition = geoInterpolate(position2, position1);
requestAnimationFrame(outAnimationFunc);
await outAnimation;
outAnimation = null;
arc = null;
}

const inAnimationFunc = () => {
if (!inAnimation) return;
projection.rotate(interpolateVersor($tween));
projection = projection;
arc = { type: 'LineString', coordinates: [position1, interpolatePosition($tween)] };
requestAnimationFrame(inAnimationFunc);
};

const outAnimationFunc = () => {
if (!outAnimation) return;
arc = { type: 'LineString', coordinates: [interpolatePosition($tween), position2] };
requestAnimationFrame(outAnimationFunc);
};

$: land = world ? feature(world, world.objects.land) : null;
$: height = getHeight(width);
$: countries = getCountries(world);
$: worldTour(name);
$: path = geoPath(projection);
</script>

<svg {width} {height} viewBox="0 0 {width} {height}">
{#if land}
<g>
<path class="sphere" d={path(sphere)} stroke="#ccc" fill="white" />
<path class="land" d={path(land)} stroke="none" fill="#ccc" />
{#if countryGraphic}
<path class="selected" d={path(countryGraphic)} fill="red" />
{/if}
{#if arc}
<path class="arc" d={path(arc)} stroke="black" stroke-width="2" fill="none" />
{/if}
</g>
{/if}
</svg>
64 changes: 64 additions & 0 deletions src/lib/Projection/Versor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export class Versor {
static fromAngles([l, p, g]: [number, number, number]): [number, number, number, number] {
l *= Math.PI / 360;
p *= Math.PI / 360;
g *= Math.PI / 360;
const sl = Math.sin(l),
cl = Math.cos(l);
const sp = Math.sin(p),
cp = Math.cos(p);
const sg = Math.sin(g),
cg = Math.cos(g);
return [
cl * cp * cg + sl * sp * sg,
sl * cp * cg - cl * sp * sg,
cl * sp * cg + sl * cp * sg,
cl * cp * sg - sl * sp * cg
];
}
static toAngles([a, b, c, d]: [number, number, number, number]): [number, number, number] {
return [
(Math.atan2(2 * (a * b + c * d), 1 - 2 * (b * b + c * c)) * 180) / Math.PI,
(Math.asin(Math.max(-1, Math.min(1, 2 * (a * c - d * b)))) * 180) / Math.PI,
(Math.atan2(2 * (a * d + b * c), 1 - 2 * (c * c + d * d)) * 180) / Math.PI
];
}
static interpolateAngles(a: [number, number, number], b: [number, number, number]) {
const i = Versor.interpolate(Versor.fromAngles(a), Versor.fromAngles(b));
return (t: number) => Versor.toAngles(i(t));
}
static interpolateLinear(
[a1, b1, c1, d1]: [number, number, number, number],
[a2, b2, c2, d2]: [number, number, number, number]
): (t: number) => [number, number, number, number] {
(a2 -= a1), (b2 -= b1), (c2 -= c1), (d2 -= d1);
const x: number[] = new Array(4);
return (t: number) => {
const l = Math.hypot(
(x[0] = a1 + a2 * t),
(x[1] = b1 + b2 * t),
(x[2] = c1 + c2 * t),
(x[3] = d1 + d2 * t)
);
(x[0] /= l), (x[1] /= l), (x[2] /= l), (x[3] /= l);
return x as [number, number, number, number];
};
}
static interpolate(
[a1, b1, c1, d1]: [number, number, number, number],
[a2, b2, c2, d2]: [number, number, number, number]
): (t: number) => [number, number, number, number] {
let dot = a1 * a2 + b1 * b2 + c1 * c2 + d1 * d2;
if (dot < 0) (a2 = -a2), (b2 = -b2), (c2 = -c2), (d2 = -d2), (dot = -dot);
if (dot > 0.9995) return Versor.interpolateLinear([a1, b1, c1, d1], [a2, b2, c2, d2]);
const theta0 = Math.acos(Math.max(-1, Math.min(1, dot)));
const l = Math.hypot((a2 -= a1 * dot), (b2 -= b1 * dot), (c2 -= c1 * dot), (d2 -= d1 * dot));
(a2 /= l), (b2 /= l), (c2 /= l), (d2 /= l);
return (t: number) => {
const theta = theta0 * t;
const s = Math.sin(theta);
const c = Math.cos(theta);
return [a1 * c + a2 * s, b1 * c + b2 * s, c1 * c + c2 * s, d1 * c + d2 * s];
};
}
}
5 changes: 5 additions & 0 deletions src/lib/Projection/tween.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { tweened } from 'svelte/motion';

export const tween = tweened(0, {
duration: 1500
});
24 changes: 24 additions & 0 deletions src/routes/projection/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import './page.css';
import Projection from '$lib/Projection/Projection.svelte';
import { feature } from 'topojson-client';
import type { PageData } from './$types';

export let data: PageData;
export let width = 720;
export let name = '';

//@ts-ignore the types package for this is wrong
$: countries = feature(data.world, data.world.objects.countries).features;
</script>

<Projection {width} {name} world={data.world} />

<input type="number" bind:value={width} />

<select bind:value={name}>
{#each countries as country}
{@const countryName = country.properties?.name}
<option value={countryName}>{countryName}</option>
{/each}
</select>
8 changes: 8 additions & 0 deletions src/routes/projection/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PageLoad } from './$types';

export const load = (async ({ fetch }) => {
const res = await fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json');
const world = await res.json();

return { world };
}) satisfies PageLoad;
3 changes: 3 additions & 0 deletions src/routes/projection/page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
html {
background-color: #262626;
}