From a052aa4bca1e0fa5c8e56e895d5f0b22a6132b34 Mon Sep 17 00:00:00 2001 From: Michael Moore Date: Wed, 30 Oct 2024 21:35:22 -0500 Subject: [PATCH] feat(map): add advanced shadows to system maps also organized system maps utils --- package-lock.json | 62 +- package.json | 4 +- src/customTypings/pathkit-wasm.d.ts | 43 ++ src/renderer/index.html | 2 +- src/renderer/src/intl/en-US.ts | 4 + .../map/solarSystemMap/SolarSystemMap.svelte | 531 ++---------------- .../lib/map/solarSystemMap/utils/colors.ts | 201 +++++++ .../map/solarSystemMap/utils/coordinates.ts | 82 +++ .../lib/map/solarSystemMap/utils/labels.ts | 117 ++++ .../lib/map/solarSystemMap/utils/planets.ts | 89 +++ .../lib/map/solarSystemMap/utils/shadows.ts | 143 +++++ src/renderer/src/lib/pathKit.ts | 3 + src/renderer/src/lib/settings/mapSettings.ts | 8 + .../src/lib/settings/mapSettingsConfig.ts | 9 + vite.config.ts | 13 +- 15 files changed, 818 insertions(+), 493 deletions(-) create mode 100644 src/customTypings/pathkit-wasm.d.ts create mode 100644 src/renderer/src/lib/map/solarSystemMap/utils/colors.ts create mode 100644 src/renderer/src/lib/map/solarSystemMap/utils/coordinates.ts create mode 100644 src/renderer/src/lib/map/solarSystemMap/utils/labels.ts create mode 100644 src/renderer/src/lib/map/solarSystemMap/utils/planets.ts create mode 100644 src/renderer/src/lib/map/solarSystemMap/utils/shadows.ts create mode 100644 src/renderer/src/lib/pathKit.ts diff --git a/package-lock.json b/package-lock.json index 2baec26..a087c24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "fast-glob": "^3.3.2", "font-list": "^1.5.1", "intl-messageformat": "^10.5.11", + "pathkit-wasm": "^1.0.0", "pixi.js": "^8.1.0", "platform-paths": "^1.2.2", "polylabel": "^1.1.0", @@ -87,7 +88,8 @@ "tailwindcss": "^3.4.3", "typescript": "^5.4.5", "utility-types": "^3.11.0", - "vite": "^5.2.8" + "vite": "^5.2.8", + "vite-plugin-static-copy": "^2.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -11009,6 +11011,11 @@ "node": ">=8" } }, + "node_modules/pathkit-wasm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pathkit-wasm/-/pathkit-wasm-1.0.0.tgz", + "integrity": "sha512-oREIcGpwnBBto6PfB3C20aIw9jBPLcSNVF1AlL5tGlXsoL2zqWVuD0mi8WMJQHHNemm6aR1hHH2lz+TO551Fgw==" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -13367,6 +13374,59 @@ } } }, + "node_modules/vite-plugin-static-copy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.0.0.tgz", + "integrity": "sha512-b/quFjTUa/RY9t3geIyeeT2GtWEoRI0GawYFFjys5iMLGgVP638NTGu0RoMjwmi8MoZZ3BQw4OQvb1GpVcXZDA==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", diff --git a/package.json b/package.json index ba98b71..415487f 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "tailwindcss": "^3.4.3", "typescript": "^5.4.5", "utility-types": "^3.11.0", - "vite": "^5.2.8" + "vite": "^5.2.8", + "vite-plugin-static-copy": "^2.0.0" }, "dependencies": { "@electron-toolkit/preload": "^3.0.1", @@ -93,6 +94,7 @@ "fast-glob": "^3.3.2", "font-list": "^1.5.1", "intl-messageformat": "^10.5.11", + "pathkit-wasm": "^1.0.0", "pixi.js": "^8.1.0", "platform-paths": "^1.2.2", "polylabel": "^1.1.0", diff --git a/src/customTypings/pathkit-wasm.d.ts b/src/customTypings/pathkit-wasm.d.ts new file mode 100644 index 0000000..074116e --- /dev/null +++ b/src/customTypings/pathkit-wasm.d.ts @@ -0,0 +1,43 @@ +// only the types used are implemented +// see https://skia.org/docs/user/modules/pathkit/#api for more +declare module 'pathkit-wasm/bin/pathkit' { + function PathKitInit(): Promise; + + export interface PathKit { + NewPath(): SkPath; + FromSVGString(str: string): SkPath; + MakeFromOp(path1: SkPath, path2: SkPath, op: PathOp): SkPath; + PathOp: { + UNION: PathOp; + DIFFERENCE: PathOp; + INTERSECT: PathOp; + XOR: PathOp; + REVERSE_DIFFERENCE: PathOp; + }; + } + + enum PathOp { + UNION, + DIFFERENCE, + INTERSECT, + XOR, + REVERSE_DIFFERENCE, + } + + export interface SkPath { + arc( + x: number, + y: number, + radius: number, + startAngle: number, + endAngle: number, + ccw = false, + ): SkPath; + op(path: SkPath, op: PathOp): SkPath; + toSVGString(): string; + delete(): void; + equals(path: SkPath): boolean; + } + + export default PathKitInit; +} diff --git a/src/renderer/index.html b/src/renderer/index.html index 56d8778..4281ebf 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -6,7 +6,7 @@ diff --git a/src/renderer/src/intl/en-US.ts b/src/renderer/src/intl/en-US.ts index 7a42de9..7a03554 100644 --- a/src/renderer/src/intl/en-US.ts +++ b/src/renderer/src/intl/en-US.ts @@ -424,6 +424,10 @@ export default { systemMapPlanetScale: 'Planet Scale', systemMapMoonScale: 'Moon Scale', systemMapPlanetRingColor: 'Planet Ring Color', + systemMapPlanetShadowSelf: 'Planets/Moons Cast Shadows on Self', + systemMapPlanetShadowPlanetarySystem: 'Planets/Moons Cast Shadows on Each Other', + systemMapPlanetShadowRings: 'Planets/Moons Cast Shadows on Rings', + systemMapPlanetShadowOverlap: 'Planet/Moons Shadow Overlap', systemMapLabelPlanetsFont: 'Planet Name Font', systemMapLabelPlanetsFontSize: 'Planet Name Font Size', systemMapLabelPlanetsPosition: 'Planet Name Position', diff --git a/src/renderer/src/lib/map/solarSystemMap/SolarSystemMap.svelte b/src/renderer/src/lib/map/solarSystemMap/SolarSystemMap.svelte index e3e43ae..4262bb4 100644 --- a/src/renderer/src/lib/map/solarSystemMap/SolarSystemMap.svelte +++ b/src/renderer/src/lib/map/solarSystemMap/SolarSystemMap.svelte @@ -4,7 +4,8 @@ import { match } from 'ts-pattern'; import { t } from '../../../intl'; - import type { GalacticObject, GameState, Planet } from '../../GameState'; + import type { GalacticObject, GameState } from '../../GameState'; + import { pathKitPromise } from '../../pathKit'; import { type MapSettings, mapSettings } from '../../settings'; import { isDefined } from '../../utils'; import { localizeText } from '../data/locUtils'; @@ -17,6 +18,27 @@ getStrokeColorAttributes, multiplyOpacity, } from '../mapUtils'; + import { getBeltColor, getPlanetColor, getStarGlowColor } from './utils/colors'; + import { + getFleetCoordinate, + getPlanetCoordinate, + getPlanetOrbitDistance, + getScaledDistance, + } from './utils/coordinates'; + import { + getPlanetLabelPathAttributes, + getPlanetLabelTextPathAttributes, + isPlanetLabeled, + } from './utils/labels'; + import { + getPlanetRadius, + getPrimaryBodies, + isAsteroid, + isPlanetarySystemPrimaryBody, + isStar, + PLANET_RING_PATTERN, + } from './utils/planets'; + import { getPathKitShadowPath } from './utils/shadows'; export let system: GalacticObject; export let gameState: GameState; @@ -139,481 +161,6 @@ return { x, y, system: toSystem, trianglePath, textPath }; }) .filter(isDefined); - - function getPlanetColor(planet: Planet): string | undefined { - let pc = planet.planet_class; - if (pc.includes('_station')) [(pc = pc.substring(0, pc.indexOf('_station')))]; - switch (pc) { - // standard habitable - case 'pc_desert': - case 'pc_arid': - case 'pc_savanna': - case 'mem_pc_death': // mem - return colors.swamp_green; - case 'pc_tropical': - case 'pc_continental': - case 'pc_ocean': - return colors.intense_blue; - case 'pc_tundra': - case 'pc_arctic': - case 'pc_alpine': - return colors.ice_turquoise; - // standard uninhabitable - case 'pc_gas_giant': - return colors.beige; - case 'pc_asteroid': - return colors.brown; - case 'pc_ice_asteroid': - return colors.faded_blue; - case 'pc_rare_crystal_asteroid': - case 'pc_crystal_asteroid': - return colors.wave_blue; - case 'pc_molten': - return colors.intense_orange; - case 'pc_barren': - case 'pc_hollow': // RS (habitable) - case 'pc_sterile': // RS (habitable) - case 'pc_tidally_locked': // RS (habitable) - return colors.khaki_brown; - case 'pc_barren_cold': - return colors.faded_blue; - case 'pc_toxic': - return colors.toxic_green; - case 'pc_frozen': - return colors.off_white; - // special - case 'pc_nuked': - case 'pc_gpm_precursor_tomb': // gpm - return colors.grey; - case 'pc_gaia': - return colors.ocean_turquoise; - case 'pc_hive': - case 'pc_infested': - return colors.sick_green; - case 'pc_shielded': - return colors.blue; - case 'pc_astral_scar': - case 'pc_shrouded': - return colors.intense_purple; - case 'pc_broken': - case 'pc_egg_cracked': - case 'pc_shattered': - case 'pc_shattered_2': - case 'pc_mem_destroyed_barren': // mem - case 'pc_mem_broken_city': // mem - return colors.brown; - // artificial - case 'pc_ai': - case 'pc_city': - case 'pc_cosmogenesis_world': - case 'pc_crystal_habitat': - case 'pc_cybrex': - case 'pc_gray_goo': - case 'pc_habitat_shielded': - case 'pc_habitat': - case 'pc_machine_broken': - case 'pc_machine': - case 'pc_nanotech': - case 'pc_ringworld_habitable_damaged': - case 'pc_ringworld_habitable': - case 'pc_ringworld_seam_damaged': - case 'pc_ringworld_seam': - case 'pc_ringworld_shielded': - case 'pc_ringworld_tech_damaged': - case 'pc_ringworld_tech': - case 'pc_shattered_ring_habitable': - case 'pc_warden_guardian': - return colors.ship_steel; - case 'pc_relic': - return colors.beige; - // stars (many of these are from RS) - case 'pc_o_star': - case 'pc_o_super_star': - case 'pc_o_hyper_star': - case 'pc_giga_o_star': // giga - return colors.cloud_purple; - case 'pc_b_star': - case 'pc_b_super_star': - return colors.light_blue; - case 'pc_a_star': - case 'pc_a_super_star': - return colors.white; - case 'pc_f_star': - case 'pc_f_super_star': - return colors.desert_yellow; - case 'pc_g_star': - case 'pc_g_giant_star': - case 'pc_g_super_star': - return colors.bright_yellow; - case 'pc_k_star': - case 'pc_k_giant_star': - case 'pc_k_super_star': - case 'pc_s_giant_star': // RS zirconium star - case 'pc_fu_star': // RS FU Orionis - return colors.light_orange; - case 'pc_m_star': - case 'pc_m_giant_star': - case 'pc_m_super_star': - case 'pc_m_hyper_star': - case 'pc_c_giant_star': // RS carbon star - case 'pc_gigaignited_star': // gaga - return colors.cerise_red; - case 'pc_l_star': // RS brown dwarf - case 'pc_t_star': // brown dwarf - case 'pc_t_star_big': // giga brown dwarf - case 'pc_y_star': // RS brown dwarf - return colors.brown; - case 'pc_black_hole': - case 'pc_mem_micro_black_hole': // mem - case 'pc_black_hole_giga': // giga - case 'pc_pouchkinn_black_hole': // giga - case 'pc_ehod_black_hole': // giga - return colors.true_black; - case 'pc_neutron_star': - case 'pc_pulsar': - case 'pc_d_star': // RS - case 'pc_collapsar': // RS - case 'pc_protostar': // RS - case 'pc_magnetar': // RS - case 'pc_microquasar_1': // RS - case 'pc_microquasar_2': // RS - case 'pc_ae_star': // RS - case 'pc_mem_synthetic_sun': // mem - case 'pc_whc_star': // giga - case 'pc_wh_star': // giga - case 'pc_whdyson_star': // giga - return colors.white; - case 'pc_w_azure_star': // RS - case 'pc_lbv_blue_star': // RS - return colors.light_blue; - case 'pc_w_red_star': // RS - case 'pc_lbv_red_star': // RS - case 'pc_tt_red_star': // RS - return colors.cerise_red; - case 'pc_tt_orange_star': // RS - return colors.light_orange; - case 'pc_w_green_star': // RS - case 'pc_lbv_green_star': // RS - return colors.sun_green; - case 'pc_w_purple_star': // RS - return colors.cloud_purple; - case 'pc_tt_white_star': // RS - return colors.white; - case 'pc_toxoid_star': - return colors.sun_green; - case 'pc_rift_star': - return colors.intense_purple; - // RS stations that don't correspond planet classes - case 'pc_cracking': - return colors.brown; - case 'pc_methane': - case 'pc_greenhouse': - case 'pc_sulfur': - return colors.toxic_green; - case 'pc_scorched': - case 'pc_hot_giant': - return colors.intense_orange; - case 'pc_ice_giant': - return colors.ice_turquoise; - // fallback - default: - console.warn(`Unhandled planet class: ${pc}; assuming megastructure`); - return colors.ship_steel; - } - } - - function getBeltColor(beltType: string) { - switch (beltType) { - case 'icy_asteroid_belt': - return colors.ice_turquoise; - case 'crystal_asteroid_belt': - return colors.wave_blue; - case 'debris_asteroid_belt': - return colors.ship_steel; - default: - return colors.brown; - } - } - - function isAsteroid(planet: Planet) { - return planet.planet_class.includes('asteroid'); - } - - function isStar(planet: Planet) { - return ( - isBlackHole(planet) || - planet.planet_class.includes('star') || - planet.planet_class === 'pc_pulsar' || - planet.planet_class === 'pc_collapsar' || - planet.planet_class === 'pc_nova_1' || - planet.planet_class === 'nova_2' - ); - } - - function isBlackHole(planet: Planet) { - return planet.planet_class.includes('black_hole'); - } - - function isColony(planet: Planet) { - return planet.owner != null; - } - - function isMoon(planet: Planet) { - return Boolean(planet.is_moon); - } - - function getPlanetRadius(planet: Planet, settings: MapSettings) { - return Math.sqrt( - planet.planet_size * - (!isMoon(planet) && !isStar(planet) ? settings.systemMapPlanetScale ?? 1 : 1) * - (isStar(planet) ? settings.systemMapStarScale ?? 1 : 1) * - (isMoon(planet) ? settings.systemMapMoonScale ?? 1 : 1), - ); - } - - function isPlanetLabeled(planet: Planet, settings: MapSettings) { - return ( - (isColony(planet) && settings.systemMapLabelColoniesEnabled) || - (isStar(planet) && settings.systemMapLabelStarsEnabled) || - (isMoon(planet) && settings.systemMapLabelMoonsEnabled) || - (isAsteroid(planet) && settings.systemMapLabelAsteroidsEnabled) || - (!isStar(planet) && - !isMoon(planet) && - !isAsteroid(planet) && - settings.systemMapLabelPlanetsEnabled) - ); - } - - function getPlanetLabelPathAttributes(planet: Planet, planets: Planet[], settings: MapSettings) { - const r = getPlanetRadius(planet, settings); - let { x, y } = getPlanetCoordinate(planet, planets, settings); - let position = settings.systemMapLabelPlanetsPosition; - if (position === 'orbit' && !planet.orbit) { - position = settings.systemMapLabelPlanetsFallbackPosition; - } - switch (position) { - case 'top': { - y -= r + settings.systemMapLabelPlanetsFontSize / 2; - x -= 500; - return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; - } - case 'bottom': { - y += r + settings.systemMapLabelPlanetsFontSize / 2; - x -= 500; - return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; - } - case 'right': { - x += r + settings.systemMapLabelPlanetsFontSize / 2; - return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; - } - case 'left': { - x -= r + settings.systemMapLabelPlanetsFontSize / 2 + 1000; - return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; - } - case 'orbit': { - const primaryBody = getPrimaryBodies(planet, planets)[0]; - const { x: cx, y: cy } = primaryBody - ? getPlanetCoordinate(primaryBody, planets, settings) - : { x: 0, y: 0 }; - const orbitRadius = getPlanetOrbitDistance(planet, planets, settings); - if (cy > y) { - return { - d: `M ${x} ${y} A ${orbitRadius} ${orbitRadius} 0 0 1 ${cx + (cx - x)} ${cy + (cy - y)}`, - }; - } else { - return { - d: `M ${x} ${y} A ${orbitRadius} ${orbitRadius} 0 0 0 ${cx + (cx - x)} ${cy + (cy - y)}`, - }; - } - } - default: { - throw new Error(`Unhandled label position: ${position}`); - } - } - } - - function getPlanetLabelTextPathAttributes(planet: Planet, settings: MapSettings) { - const r = getPlanetRadius(planet, settings); - let position = settings.systemMapLabelPlanetsPosition; - if (position === 'orbit' && !planet.orbit) { - position = settings.systemMapLabelPlanetsFallbackPosition; - } - switch (position) { - case 'top': { - return { - startOffset: 0.5, - 'dominant-baseline': 'auto', - 'text-anchor': 'middle', - }; - } - case 'bottom': { - return { - startOffset: 0.5, - 'dominant-baseline': 'hanging', - 'text-anchor': 'middle', - }; - } - case 'right': { - return { - startOffset: 0, - 'dominant-baseline': 'middle', - 'text-anchor': 'start', - }; - } - case 'left': { - return { - startOffset: 1, - 'dominant-baseline': 'middle', - 'text-anchor': 'end', - }; - } - case 'orbit': { - return { - startOffset: r + settings.systemMapLabelPlanetsFontSize / 2, - 'dominant-baseline': 'auto', - 'text-anchor': 'start', - }; - } - default: { - throw new Error(`Unhandled label position: ${position}`); - } - } - } - - function getShadowPath(planet: Planet, settings: MapSettings) { - const r = getPlanetRadius(planet, settings); - return ` - M 0 ${r} - A ${r} ${r} 0 0 0 0 ${-r} - A ${r * 0.5} ${r} 0 0 1 0 ${r} - Z - `; - } - - function getShadowRotation(planet: Planet) { - const angle = (Math.atan(planet.coordinate.y / -planet.coordinate.x) / Math.PI / 2) * 360; - if (planet.coordinate.x > 0) return angle + 180; - return angle; - } - - function getScaledDistance(unscaledDistance: number, settings: MapSettings) { - const exponent = settings.systemMapOrbitDistanceExponent; - return (unscaledDistance ** exponent / 400 ** exponent) * 400; - } - - function getScaledCoordinate( - coordinate: { x: number; y: number }, - relativeTo: { x: number; y: number; r: number }[], - settings: MapSettings, - ): { x: number; y: number } { - const unscaledDistance = Math.hypot( - coordinate.x - (relativeTo[0]?.x ?? 0), - coordinate.y - (relativeTo[0]?.y ?? 0), - ); - const scaledDistance = - (relativeTo[0]?.r ?? 0) + - getScaledDistance(unscaledDistance - (relativeTo[0]?.r ?? 0), settings); - const theta = Math.atan2( - coordinate.y - (relativeTo[0]?.y ?? 0), - coordinate.x - (relativeTo[0]?.x ?? 0), - ); - const scaledRelativeTo = relativeTo[0] - ? getScaledCoordinate(relativeTo[0], relativeTo.slice(1), settings) - : { x: 0, y: 0 }; - const x = scaledRelativeTo.x + Math.cos(theta) * scaledDistance; - const y = scaledRelativeTo.y + Math.sin(theta) * scaledDistance; - return { x, y }; - } - - function flipX(coordinate: T): T { - return { ...coordinate, x: -coordinate.x }; - } - - function getPrimaryBodies(planet: Planet, planets: Planet[]) { - const primaryBodies: Planet[] = []; - let done = false; - let planetToCheck: Planet = planet; - while (!done) { - let primaryBody = planets.find((p) => p.id === planetToCheck.moon_of); - if (!primaryBody && !(planetToCheck.coordinate.x === 0 && planetToCheck.coordinate.y === 0)) { - primaryBody = planets.find((p) => p.coordinate.x === 0 && p.coordinate.y === 0); - } - if (primaryBody) { - primaryBodies.push(primaryBody); - planetToCheck = primaryBody; - } else { - done = true; - } - } - return primaryBodies; - } - - function getPlanetCoordinate(planet: Planet, planets: Planet[], settings: MapSettings) { - return getScaledCoordinate( - flipX(planet.coordinate), - getPrimaryBodies(planet, planets).map((p) => ({ - ...flipX(p.coordinate), - r: getPlanetRadius(p, settings), - })), - settings, - ); - } - - function getPlanetOrbitDistance(planet: Planet, planets: Planet[], settings: MapSettings) { - const primary = getPrimaryBodies(planet, planets)[0]; - const primaryR = primary ? getPlanetRadius(primary, settings) : 0; - const unscaledDistance = Math.hypot( - planet.coordinate.x - (primary?.coordinate.x ?? 0), - planet.coordinate.y - (primary?.coordinate.y ?? 0), - ); - const scaledDistance = primaryR + getScaledDistance(unscaledDistance - primaryR, settings); - return scaledDistance; - } - - function getFleetCoordinate( - fleet: (typeof fleets)[number], - planets: Planet[], - settings: MapSettings, - ) { - const closestPlanet = planets.toSorted( - (a, b) => - (-a.coordinate.x - fleet.coordinate.x) ** 2 + - (a.coordinate.y - fleet.coordinate.y) ** 2 - - (-b.coordinate.x - fleet.coordinate.x) ** 2 - - (b.coordinate.y - fleet.coordinate.y) ** 2, - )[0]; - return getScaledCoordinate( - fleet.coordinate, - closestPlanet - ? [closestPlanet, ...getPrimaryBodies(closestPlanet, planets)].map((p) => ({ - ...flipX(p.coordinate), - r: getPlanetRadius(p, settings), - })) - : [], - settings, - ); - } - - const PLANET_RING_PATTERN = ( - [ - [0.3, 0], - [0.05, 0.05], - [0.3, 0.15], - [0.1, 0.5], - [0.1, 1], - [0.02, 0.5], - [0.1, 1], - [0.1, 0.5], - [0.05, 0], - [0.2, 0.3], - [0.02, 0], - [0.05, 0.5], - ] as [number, number][] - ).map(([width, opacity], index, array) => { - const radiusMultiplier = - array.slice(0, index).reduce((total, [curWidth]) => total + curWidth, 0) + 1 + width / 2; - return { width, opacity, radiusMultiplier }; - }); {:else} {@const radius = getPlanetRadius(planet, $mapSettings)} - - {#if planet.has_ring} {#each PLANET_RING_PATTERN as ring} @@ -744,13 +290,20 @@ mapSettings: $mapSettings, colors, colorStack: [multiplyOpacity($mapSettings.systemMapPlanetRingColor, ring.opacity)], - planetColor: getPlanetColor(planet), + planetColor: getPlanetColor(planet, colors), })} /> {/each} {/if} {/if} {/each} + {#each planets.filter((p) => isPlanetarySystemPrimaryBody(p, planets)) as planet (planet.id)} + {#await pathKitPromise then PathKit} + {#each getPathKitShadowPath(planet, planets, $mapSettings, PathKit) as d} + + {/each} + {/await} + {/each} {#each planets.filter((p) => isPlanetLabeled(p, $mapSettings)) as planet (planet.id)} ): string | undefined { + let pc = planet.planet_class; + if (pc.includes('_station')) [(pc = pc.substring(0, pc.indexOf('_station')))]; + switch (pc) { + // standard habitable + case 'pc_desert': + case 'pc_arid': + case 'pc_savanna': + case 'mem_pc_death': // mem + return colors.swamp_green; + case 'pc_tropical': + case 'pc_continental': + case 'pc_ocean': + return colors.intense_blue; + case 'pc_tundra': + case 'pc_arctic': + case 'pc_alpine': + return colors.ice_turquoise; + // standard uninhabitable + case 'pc_gas_giant': + return colors.beige; + case 'pc_asteroid': + return colors.brown; + case 'pc_ice_asteroid': + return colors.faded_blue; + case 'pc_rare_crystal_asteroid': + case 'pc_crystal_asteroid': + return colors.wave_blue; + case 'pc_molten': + return colors.intense_orange; + case 'pc_barren': + case 'pc_hollow': // RS (habitable) + case 'pc_sterile': // RS (habitable) + case 'pc_tidally_locked': // RS (habitable) + return colors.khaki_brown; + case 'pc_barren_cold': + return colors.faded_blue; + case 'pc_toxic': + return colors.toxic_green; + case 'pc_frozen': + return colors.off_white; + // special + case 'pc_nuked': + case 'pc_gpm_precursor_tomb': // gpm + return colors.grey; + case 'pc_gaia': + return colors.ocean_turquoise; + case 'pc_hive': + case 'pc_infested': + return colors.sick_green; + case 'pc_shielded': + return colors.blue; + case 'pc_astral_scar': + case 'pc_shrouded': + return colors.intense_purple; + case 'pc_broken': + case 'pc_egg_cracked': + case 'pc_shattered': + case 'pc_shattered_2': + case 'pc_mem_destroyed_barren': // mem + case 'pc_mem_broken_city': // mem + return colors.brown; + // artificial + case 'pc_ai': + case 'pc_city': + case 'pc_cosmogenesis_world': + case 'pc_crystal_habitat': + case 'pc_cybrex': + case 'pc_gray_goo': + case 'pc_habitat_shielded': + case 'pc_habitat': + case 'pc_machine_broken': + case 'pc_machine': + case 'pc_nanotech': + case 'pc_ringworld_habitable_damaged': + case 'pc_ringworld_habitable': + case 'pc_ringworld_seam_damaged': + case 'pc_ringworld_seam': + case 'pc_ringworld_shielded': + case 'pc_ringworld_tech_damaged': + case 'pc_ringworld_tech': + case 'pc_shattered_ring_habitable': + case 'pc_warden_guardian': + return colors.ship_steel; + case 'pc_relic': + return colors.beige; + // stars (many of these are from RS) + case 'pc_o_star': + case 'pc_o_super_star': + case 'pc_o_hyper_star': + case 'pc_giga_o_star': // giga + return colors.cloud_purple; + case 'pc_b_star': + case 'pc_b_super_star': + return colors.light_blue; + case 'pc_a_star': + case 'pc_a_super_star': + return colors.white; + case 'pc_f_star': + case 'pc_f_super_star': + return colors.desert_yellow; + case 'pc_g_star': + case 'pc_g_giant_star': + case 'pc_g_super_star': + return colors.bright_yellow; + case 'pc_k_star': + case 'pc_k_giant_star': + case 'pc_k_super_star': + case 'pc_s_giant_star': // RS zirconium star + case 'pc_fu_star': // RS FU Orionis + return colors.light_orange; + case 'pc_m_star': + case 'pc_m_giant_star': + case 'pc_m_super_star': + case 'pc_m_hyper_star': + case 'pc_c_giant_star': // RS carbon star + case 'pc_gigaignited_star': // gaga + return colors.cerise_red; + case 'pc_l_star': // RS brown dwarf + case 'pc_t_star': // brown dwarf + case 'pc_t_star_big': // giga brown dwarf + case 'pc_y_star': // RS brown dwarf + return colors.brown; + case 'pc_black_hole': + case 'pc_mem_micro_black_hole': // mem + case 'pc_black_hole_giga': // giga + case 'pc_pouchkinn_black_hole': // giga + case 'pc_ehod_black_hole': // giga + return colors.true_black; + case 'pc_neutron_star': + case 'pc_pulsar': + case 'pc_d_star': // RS + case 'pc_collapsar': // RS + case 'pc_protostar': // RS + case 'pc_magnetar': // RS + case 'pc_microquasar_1': // RS + case 'pc_microquasar_2': // RS + case 'pc_ae_star': // RS + case 'pc_mem_synthetic_sun': // mem + case 'pc_whc_star': // giga + case 'pc_wh_star': // giga + case 'pc_whdyson_star': // giga + return colors.white; + case 'pc_w_azure_star': // RS + case 'pc_lbv_blue_star': // RS + return colors.light_blue; + case 'pc_w_red_star': // RS + case 'pc_lbv_red_star': // RS + case 'pc_tt_red_star': // RS + return colors.cerise_red; + case 'pc_tt_orange_star': // RS + return colors.light_orange; + case 'pc_w_green_star': // RS + case 'pc_lbv_green_star': // RS + return colors.sun_green; + case 'pc_w_purple_star': // RS + return colors.cloud_purple; + case 'pc_tt_white_star': // RS + return colors.white; + case 'pc_toxoid_star': + return colors.sun_green; + case 'pc_rift_star': + return colors.intense_purple; + // RS stations that don't correspond planet classes + case 'pc_cracking': + return colors.brown; + case 'pc_methane': + case 'pc_greenhouse': + case 'pc_sulfur': + return colors.toxic_green; + case 'pc_scorched': + case 'pc_hot_giant': + return colors.intense_orange; + case 'pc_ice_giant': + return colors.ice_turquoise; + // fallback + default: + console.warn(`Unhandled planet class: ${pc}; assuming megastructure`); + return colors.ship_steel; + } +} + +export function getStarGlowColor(star: Planet, colors: Record) { + return isBlackHole(star) ? '#FFFFFF' : getPlanetColor(star, colors); +} + +export function getBeltColor(beltType: string, colors: Record) { + switch (beltType) { + case 'icy_asteroid_belt': + return colors.ice_turquoise; + case 'crystal_asteroid_belt': + return colors.wave_blue; + case 'debris_asteroid_belt': + return colors.ship_steel; + default: + return colors.brown; + } +} diff --git a/src/renderer/src/lib/map/solarSystemMap/utils/coordinates.ts b/src/renderer/src/lib/map/solarSystemMap/utils/coordinates.ts new file mode 100644 index 0000000..60c90b5 --- /dev/null +++ b/src/renderer/src/lib/map/solarSystemMap/utils/coordinates.ts @@ -0,0 +1,82 @@ +import type { Planet } from '../../../GameState'; +import type { MapSettings } from '../../../settings'; +import { getPlanetRadius, getPrimaryBodies } from './planets'; + +export function getScaledDistance(unscaledDistance: number, settings: MapSettings) { + const exponent = settings.systemMapOrbitDistanceExponent; + return (unscaledDistance ** exponent / 400 ** exponent) * 400; +} + +function getScaledCoordinate( + coordinate: { x: number; y: number }, + relativeTo: { x: number; y: number; r: number }[], + settings: MapSettings, +): { x: number; y: number } { + const unscaledDistance = Math.hypot( + coordinate.x - (relativeTo[0]?.x ?? 0), + coordinate.y - (relativeTo[0]?.y ?? 0), + ); + const scaledDistance = + (relativeTo[0]?.r ?? 0) + + getScaledDistance(unscaledDistance - (relativeTo[0]?.r ?? 0), settings); + const theta = Math.atan2( + coordinate.y - (relativeTo[0]?.y ?? 0), + coordinate.x - (relativeTo[0]?.x ?? 0), + ); + const scaledRelativeTo = relativeTo[0] + ? getScaledCoordinate(relativeTo[0], relativeTo.slice(1), settings) + : { x: 0, y: 0 }; + const x = scaledRelativeTo.x + Math.cos(theta) * scaledDistance; + const y = scaledRelativeTo.y + Math.sin(theta) * scaledDistance; + return { x, y }; +} + +function flipX(coordinate: T): T { + return { ...coordinate, x: -coordinate.x }; +} + +export function getFleetCoordinate( + fleet: { coordinate: { x: number; y: number } }, + planets: Planet[], + settings: MapSettings, +) { + const closestPlanet = planets.toSorted( + (a, b) => + (-a.coordinate.x - fleet.coordinate.x) ** 2 + + (a.coordinate.y - fleet.coordinate.y) ** 2 - + (-b.coordinate.x - fleet.coordinate.x) ** 2 - + (b.coordinate.y - fleet.coordinate.y) ** 2, + )[0]; + return getScaledCoordinate( + fleet.coordinate, + closestPlanet + ? [closestPlanet, ...getPrimaryBodies(closestPlanet, planets)].map((p) => ({ + ...flipX(p.coordinate), + r: getPlanetRadius(p, settings), + })) + : [], + settings, + ); +} + +export function getPlanetCoordinate(planet: Planet, planets: Planet[], settings: MapSettings) { + return getScaledCoordinate( + flipX(planet.coordinate), + getPrimaryBodies(planet, planets).map((p) => ({ + ...flipX(p.coordinate), + r: getPlanetRadius(p, settings), + })), + settings, + ); +} + +export function getPlanetOrbitDistance(planet: Planet, planets: Planet[], settings: MapSettings) { + const primary = getPrimaryBodies(planet, planets)[0]; + const primaryR = primary ? getPlanetRadius(primary, settings) : 0; + const unscaledDistance = Math.hypot( + planet.coordinate.x - (primary?.coordinate.x ?? 0), + planet.coordinate.y - (primary?.coordinate.y ?? 0), + ); + const scaledDistance = primaryR + getScaledDistance(unscaledDistance - primaryR, settings); + return scaledDistance; +} diff --git a/src/renderer/src/lib/map/solarSystemMap/utils/labels.ts b/src/renderer/src/lib/map/solarSystemMap/utils/labels.ts new file mode 100644 index 0000000..1b198fa --- /dev/null +++ b/src/renderer/src/lib/map/solarSystemMap/utils/labels.ts @@ -0,0 +1,117 @@ +import type { Planet } from '../../../GameState'; +import type { MapSettings } from '../../../settings'; +import { getPlanetCoordinate, getPlanetOrbitDistance } from './coordinates'; +import { getPlanetRadius, getPrimaryBodies, isAsteroid, isColony, isMoon, isStar } from './planets'; + +export function isPlanetLabeled(planet: Planet, settings: MapSettings) { + return ( + (isColony(planet) && settings.systemMapLabelColoniesEnabled) || + (isStar(planet) && settings.systemMapLabelStarsEnabled) || + (isMoon(planet) && settings.systemMapLabelMoonsEnabled) || + (isAsteroid(planet) && settings.systemMapLabelAsteroidsEnabled) || + (!isStar(planet) && + !isMoon(planet) && + !isAsteroid(planet) && + settings.systemMapLabelPlanetsEnabled) + ); +} + +export function getPlanetLabelPathAttributes( + planet: Planet, + planets: Planet[], + settings: MapSettings, +) { + const r = getPlanetRadius(planet, settings); + let { x, y } = getPlanetCoordinate(planet, planets, settings); + let position = settings.systemMapLabelPlanetsPosition; + if (position === 'orbit' && !planet.orbit) { + position = settings.systemMapLabelPlanetsFallbackPosition; + } + switch (position) { + case 'top': { + y -= r + settings.systemMapLabelPlanetsFontSize / 2; + x -= 500; + return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; + } + case 'bottom': { + y += r + settings.systemMapLabelPlanetsFontSize / 2; + x -= 500; + return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; + } + case 'right': { + x += r + settings.systemMapLabelPlanetsFontSize / 2; + return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; + } + case 'left': { + x -= r + settings.systemMapLabelPlanetsFontSize / 2 + 1000; + return { d: `M ${x} ${y} h 1000`, pathLength: 1 }; + } + case 'orbit': { + const primaryBody = getPrimaryBodies(planet, planets)[0]; + const { x: cx, y: cy } = primaryBody + ? getPlanetCoordinate(primaryBody, planets, settings) + : { x: 0, y: 0 }; + const orbitRadius = getPlanetOrbitDistance(planet, planets, settings); + if (cy > y) { + return { + d: `M ${x} ${y} A ${orbitRadius} ${orbitRadius} 0 0 1 ${cx + (cx - x)} ${cy + (cy - y)}`, + }; + } else { + return { + d: `M ${x} ${y} A ${orbitRadius} ${orbitRadius} 0 0 0 ${cx + (cx - x)} ${cy + (cy - y)}`, + }; + } + } + default: { + throw new Error(`Unhandled label position: ${position}`); + } + } +} + +export function getPlanetLabelTextPathAttributes(planet: Planet, settings: MapSettings) { + const r = getPlanetRadius(planet, settings); + let position = settings.systemMapLabelPlanetsPosition; + if (position === 'orbit' && !planet.orbit) { + position = settings.systemMapLabelPlanetsFallbackPosition; + } + switch (position) { + case 'top': { + return { + startOffset: 0.5, + 'dominant-baseline': 'auto', + 'text-anchor': 'middle', + }; + } + case 'bottom': { + return { + startOffset: 0.5, + 'dominant-baseline': 'hanging', + 'text-anchor': 'middle', + }; + } + case 'right': { + return { + startOffset: 0, + 'dominant-baseline': 'middle', + 'text-anchor': 'start', + }; + } + case 'left': { + return { + startOffset: 1, + 'dominant-baseline': 'middle', + 'text-anchor': 'end', + }; + } + case 'orbit': { + return { + startOffset: r + settings.systemMapLabelPlanetsFontSize / 2, + 'dominant-baseline': 'auto', + 'text-anchor': 'start', + }; + } + default: { + throw new Error(`Unhandled label position: ${position}`); + } + } +} diff --git a/src/renderer/src/lib/map/solarSystemMap/utils/planets.ts b/src/renderer/src/lib/map/solarSystemMap/utils/planets.ts new file mode 100644 index 0000000..b7ff73b --- /dev/null +++ b/src/renderer/src/lib/map/solarSystemMap/utils/planets.ts @@ -0,0 +1,89 @@ +import type { Planet } from '../../../GameState'; +import type { MapSettings } from '../../../settings'; + +export function isAsteroid(planet: Planet) { + return planet.planet_class.includes('asteroid'); +} + +export function isStar(planet: Planet) { + return ( + isBlackHole(planet) || + planet.planet_class.includes('star') || + planet.planet_class === 'pc_pulsar' || + planet.planet_class === 'pc_collapsar' || + planet.planet_class === 'pc_nova_1' || + planet.planet_class === 'nova_2' + ); +} + +export function isBlackHole(planet: Planet) { + return planet.planet_class.includes('black_hole'); +} + +export function isColony(planet: Planet) { + return planet.owner != null; +} + +export function isMoon(planet: Planet) { + return Boolean(planet.is_moon); +} + +export function getPlanetRadius(planet: Planet, settings: MapSettings) { + return Math.sqrt( + planet.planet_size * + (!isMoon(planet) && !isStar(planet) ? settings.systemMapPlanetScale ?? 1 : 1) * + (isStar(planet) ? settings.systemMapStarScale ?? 1 : 1) * + (isMoon(planet) ? settings.systemMapMoonScale ?? 1 : 1), + ); +} + +export function getPlanetStar(planet: Planet, planets: Planet[]) { + return getPrimaryBodies(planet, planets).find(isStar); +} + +export function isPlanetarySystemPrimaryBody(planet: Planet, planets: Planet[]) { + if (isStar(planet)) return false; + const primary = getPrimaryBodies(planet, planets)[0]; + if (primary && !isStar(primary)) return false; + return true; +} + +export function getPrimaryBodies(planet: Planet, planets: Planet[]) { + const primaryBodies: Planet[] = []; + let done = false; + let planetToCheck: Planet = planet; + while (!done) { + let primaryBody = planets.find((p) => p.id === planetToCheck.moon_of); + if (!primaryBody && !(planetToCheck.coordinate.x === 0 && planetToCheck.coordinate.y === 0)) { + primaryBody = planets.find((p) => p.coordinate.x === 0 && p.coordinate.y === 0); + } + if (primaryBody) { + primaryBodies.push(primaryBody); + planetToCheck = primaryBody; + } else { + done = true; + } + } + return primaryBodies; +} + +export const PLANET_RING_PATTERN = ( + [ + [0.3, 0], + [0.05, 0.05], + [0.3, 0.15], + [0.1, 0.5], + [0.1, 1], + [0.02, 0.5], + [0.1, 1], + [0.1, 0.5], + [0.05, 0], + [0.2, 0.3], + [0.02, 0], + [0.05, 0.5], + ] as [number, number][] +).map(([width, opacity], index, array) => { + const radiusMultiplier = + array.slice(0, index).reduce((total, [curWidth]) => total + curWidth, 0) + 1 + width / 2; + return { width, opacity, radiusMultiplier }; +}); diff --git a/src/renderer/src/lib/map/solarSystemMap/utils/shadows.ts b/src/renderer/src/lib/map/solarSystemMap/utils/shadows.ts new file mode 100644 index 0000000..5d8b16c --- /dev/null +++ b/src/renderer/src/lib/map/solarSystemMap/utils/shadows.ts @@ -0,0 +1,143 @@ +import type { PathKit } from 'pathkit-wasm/bin/pathkit'; + +import type { Planet } from '../../../GameState'; +import type { MapSettings } from '../../../settings'; +import { getPlanetCoordinate } from './coordinates'; +import { getPlanetRadius, getPlanetStar, PLANET_RING_PATTERN } from './planets'; + +export function getPathKitShadowPath( + planet: Planet, + planets: Planet[], + settings: MapSettings, + PathKit: PathKit, +) { + const overlapPaths = []; + const moons = planets.filter((p) => p.moon_of === planet.id); + + const shadow = PathKit.FromSVGString(getPlanetCastShadowPath(planet, planets, settings)); + for (const moon of moons) { + const moonShadow = PathKit.FromSVGString(getPlanetCastShadowPath(moon, planets, settings)); + shadow.op(moonShadow, PathKit.PathOp.UNION); + } + + const shadowed = PathKit.NewPath(); + + if (settings.systemMapPlanetShadowRings && planet.has_ring) { + const { x, y } = getPlanetCoordinate(planet, planets, settings); + const planetR = getPlanetRadius(planet, settings); + const innerRing = PLANET_RING_PATTERN.find((p) => p.opacity > 0); + const innerRingR = innerRing + ? innerRing.radiusMultiplier * planetR - (innerRing.width * planetR) / 2 + : 0; + const outerRing = PLANET_RING_PATTERN.at(-1); + const outerRingR = outerRing + ? outerRing.radiusMultiplier * planetR + (outerRing.width * planetR) / 2 + : 0; + const ring = PathKit.NewPath(); + ring.arc(x, y, outerRingR, 0, Math.PI * 2); + const inner = PathKit.NewPath(); + inner.arc(x, y, innerRingR, 0, Math.PI * 2); + ring.op(inner, PathKit.PathOp.DIFFERENCE); + shadowed.op(ring, PathKit.PathOp.UNION); + ring.delete(); + inner.delete(); + } + + // we don't planets casting on each other, so clear out the those shadows + // but keep any intersection of those shadows with the ring (the only thing in shadowed currently) + if (!settings.systemMapPlanetShadowPlanetarySystem) { + shadow.op(shadowed, PathKit.PathOp.INTERSECT); + } + + for (const p of [planet, ...moons]) { + if (settings.systemMapPlanetShadowSelf) { + const planetSelfShadowPath = PathKit.FromSVGString( + getPlanetSelfShadowPath(p, planets, settings), + ); + if (settings.systemMapPlanetShadowOverlap) { + const overlap = PathKit.MakeFromOp(planetSelfShadowPath, shadow, PathKit.PathOp.INTERSECT); + overlapPaths.push(overlap.toSVGString()); + overlap.delete(); + } + shadow.op(planetSelfShadowPath, PathKit.PathOp.UNION); + planetSelfShadowPath.delete(); + } + + const { x, y } = getPlanetCoordinate(p, planets, settings); + const r = getPlanetRadius(p, settings); + const planetPath = PathKit.NewPath(); + planetPath.arc(x, y, r, 0, Math.PI * 2); + shadowed.op(planetPath, PathKit.PathOp.UNION); + planetPath.delete(); + } + + const combined = PathKit.MakeFromOp(shadow, shadowed, PathKit.PathOp.INTERSECT); + const svgString = combined.toSVGString(); + if (planet.has_ring) { + console.log({ + svgString, + combinedEqShadow: combined.equals(shadow), + combinedEqShadowed: combined.equals(shadowed), + }); + } + shadow.delete(); + shadowed.delete(); + combined.delete(); + + return [svgString, ...overlapPaths]; +} + +function getPlanetSelfShadowPath(planet: Planet, planets: Planet[], settings: MapSettings) { + const { x, y } = getPlanetCoordinate(planet, planets, settings); + const star = getPlanetStar(planet, planets); + const starCoordinate = star ? getPlanetCoordinate(star, planets, settings) : { x: 0, y: 0 }; + const theta = Math.atan2(y - starCoordinate.y, x - starCoordinate.x); + const perpendicular = theta + Math.PI / 2; + const r = getPlanetRadius(planet, settings); + const p1 = { + x: x + Math.cos(perpendicular) * r, + y: y + Math.sin(perpendicular) * r, + }; + const p2 = { + x: x - Math.cos(perpendicular) * r, + y: y - Math.sin(perpendicular) * r, + }; + return ` + M ${p1.x} ${p1.y} + A ${r} ${r} 0 0 0 ${p2.x} ${p2.y} + A ${r * 1.125} ${r * 1.125} 0 0 1 ${p1.x} ${p1.y} + Z + `; +} + +function getPlanetCastShadowPath(planet: Planet, planets: Planet[], settings: MapSettings) { + const { x, y } = getPlanetCoordinate(planet, planets, settings); + const star = getPlanetStar(planet, planets); + const starCoordinate = star ? getPlanetCoordinate(star, planets, settings) : { x: 0, y: 0 }; + const theta = Math.atan2(y - starCoordinate.y, x - starCoordinate.x); + const perpendicular = theta + Math.PI / 2; + const r = getPlanetRadius(planet, settings); + const p1 = { + x: x + Math.cos(perpendicular) * r, + y: y + Math.sin(perpendicular) * r, + }; + const p2 = { + x: x - Math.cos(perpendicular) * r, + y: y - Math.sin(perpendicular) * r, + }; + const p3 = { + x: p2.x + Math.cos(theta) * 1000, + y: p2.y + Math.sin(theta) * 1000, + }; + const p4 = { + x: p1.x + Math.cos(theta) * 1000, + y: p1.y + Math.sin(theta) * 1000, + }; + return ` + M ${p1.x} ${p1.y} + A ${r} ${r} 0 0 0 ${p2.x} ${p2.y} + L ${p3.x} ${p3.y} + L ${p4.x} ${p4.y} + Z + `; +} diff --git a/src/renderer/src/lib/pathKit.ts b/src/renderer/src/lib/pathKit.ts new file mode 100644 index 0000000..2901d2e --- /dev/null +++ b/src/renderer/src/lib/pathKit.ts @@ -0,0 +1,3 @@ +import PathKitInit from 'pathkit-wasm/bin/pathkit'; + +export const pathKitPromise = PathKitInit(); diff --git a/src/renderer/src/lib/settings/mapSettings.ts b/src/renderer/src/lib/settings/mapSettings.ts index 9d1bc9d..bdc5f13 100644 --- a/src/renderer/src/lib/settings/mapSettings.ts +++ b/src/renderer/src/lib/settings/mapSettings.ts @@ -74,6 +74,10 @@ export type BooleanMapSettings = | 'systemMapLabelMoonsEnabled' | 'systemMapLabelPlanetsEnabled' | 'systemMapLabelStarsEnabled' + | 'systemMapPlanetShadowSelf' + | 'systemMapPlanetShadowPlanetarySystem' + | 'systemMapPlanetShadowRings' + | 'systemMapPlanetShadowOverlap' | 'terraIncognita' | 'unionLeaderUnderline' | 'unionMode'; @@ -504,6 +508,10 @@ export const defaultMapSettings: MapSettings = { colorAdjustments: [{ type: 'OPACITY', value: 0.75 }], }, systemMapOrbitDistanceExponent: 1, + systemMapPlanetShadowSelf: true, + systemMapPlanetShadowPlanetarySystem: true, + systemMapPlanetShadowRings: true, + systemMapPlanetShadowOverlap: false, }; export const mapSettings = localStorageStore('mapSettings', defaultMapSettings); diff --git a/src/renderer/src/lib/settings/mapSettingsConfig.ts b/src/renderer/src/lib/settings/mapSettingsConfig.ts index b09f887..739dbe4 100644 --- a/src/renderer/src/lib/settings/mapSettingsConfig.ts +++ b/src/renderer/src/lib/settings/mapSettingsConfig.ts @@ -762,6 +762,15 @@ export const mapSettingsConfig: MapSettingConfigGroup[] = [ type: 'color', allowedDynamicColors: PLANET_SCOPED_DYNAMIC_COLORS, }, + { id: 'systemMapPlanetShadowSelf', type: 'toggle' }, + { id: 'systemMapPlanetShadowPlanetarySystem', type: 'toggle' }, + { id: 'systemMapPlanetShadowRings', type: 'toggle' }, + { + id: 'systemMapPlanetShadowOverlap', + type: 'toggle', + hideIf: (settings) => + !settings.systemMapPlanetShadowSelf || !settings.systemMapPlanetShadowPlanetarySystem, + }, { id: 'systemMapLabelPlanetsFont', type: 'select', diff --git a/vite.config.ts b/vite.config.ts index 628a82d..7b29d0f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,19 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'; import { defineConfig } from 'vite'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; export default defineConfig({ - plugins: [svelte({ configFile: '../../svelte.config.mjs' })], + plugins: [ + svelte({ configFile: '../../svelte.config.mjs' }), + viteStaticCopy({ + targets: [ + { + src: '../../node_modules/pathkit-wasm/bin/pathkit.wasm', + dest: '/', + }, + ], + }), + ], root: 'src/renderer', build: { outDir: '../../build',