Add character release timeline

pull/1/head
Made Baruna 2022-09-06 23:54:17 +07:00
parent 9940e0c10a
commit 9bdf631bce
7 changed files with 298 additions and 2 deletions

View File

@ -106,6 +106,7 @@
'artifacts',
'radiant-spincrystal',
'calendar',
'banners',
].includes(segment)}
image="/images/items.png"
label={$t('sidebar.database')}
@ -119,6 +120,7 @@
{ label: $t('sidebar.fishing'), href: '/fishing' },
{ label: $t('sidebar.radiantSpincrystal'), href: '/radiant-spincrystal' },
{ label: $t('sidebar.calendar'), href: '/calendar' },
{ label: $t('sidebar.banners'), href: '/banners' },
]}
/>
<SidebarItem

View File

@ -30,6 +30,7 @@ export const banners = {
timezoneDependent: true,
featured: ['venti'],
featuredRare: ['barbara', 'fischl', 'xiangling'],
version: '1.0',
},
{
name: 'Sparkling Steps',
@ -40,6 +41,7 @@ export const banners = {
color: '#CA360E',
featured: ['klee'],
featuredRare: ['xingqiu', 'sucrose', 'noelle'],
version: '1.0',
},
{
name: 'Farewell of Snezhnaya',
@ -51,6 +53,7 @@ export const banners = {
timezoneDependent: true,
featured: ['tartaglia'],
featuredRare: ['ningguang', 'beidou', 'diona'],
version: '1.1',
},
{
name: 'Gentry of Hermitage',
@ -61,6 +64,7 @@ export const banners = {
color: '#D1A55C',
featured: ['zhongli'],
featuredRare: ['xinyan', 'razor', 'chongyun'],
version: '1.1',
},
{
name: 'Secretum Secretorum',
@ -72,6 +76,7 @@ export const banners = {
timezoneDependent: true,
featured: ['albedo'],
featuredRare: ['fischl', 'bennett', 'sucrose'],
version: '1.2',
},
{
name: 'Adrift in the Harbor',
@ -82,6 +87,7 @@ export const banners = {
color: '#6994DF',
featured: ['ganyu'],
featuredRare: ['xingqiu', 'xiangling', 'noelle'],
version: '1.2',
},
{
name: 'Invitation to Mundane Life',
@ -93,6 +99,7 @@ export const banners = {
timezoneDependent: true,
featured: ['xiao'],
featuredRare: ['diona', 'xinyan', 'beidou'],
version: '1.3',
},
{
name: 'Dance of Lanterns',
@ -103,6 +110,7 @@ export const banners = {
color: '#AB6CD7',
featured: ['keqing'],
featuredRare: ['barbara', 'bennett', 'ningguang'],
version: '1.3',
},
{
name: 'Moment of Bloom',
@ -113,6 +121,7 @@ export const banners = {
color: '#BF5042',
featured: ['hu_tao'],
featuredRare: ['xingqiu', 'xiangling', 'chongyun'],
version: '1.3',
},
{
name: 'Ballad in Goblets',
@ -124,6 +133,7 @@ export const banners = {
featured: ['venti'],
featuredRare: ['sucrose', 'razor', 'noelle'],
timezoneDependent: true,
version: '1.4',
},
{
name: 'Farewell of Snezhnaya',
@ -134,6 +144,7 @@ export const banners = {
color: '#50A3C0',
featured: ['tartaglia'],
featuredRare: ['rosaria', 'fischl', 'barbara'],
version: '1.4',
},
{
name: 'Gentry of Hermitage',
@ -145,6 +156,7 @@ export const banners = {
featured: ['zhongli'],
featuredRare: ['yanfei', 'noelle', 'diona'],
timezoneDependent: true,
version: '1.5',
},
{
name: 'Born of Ocean Swell',
@ -155,6 +167,7 @@ export const banners = {
color: '#A6D6E0',
featured: ['eula'],
featuredRare: ['xingqiu', 'beidou', 'xinyan'],
version: '1.5',
},
{
name: 'Sparkling Steps',
@ -166,6 +179,7 @@ export const banners = {
featured: ['klee'],
featuredRare: ['fischl', 'sucrose', 'barbara'],
timezoneDependent: true,
version: '1.6',
},
{
name: 'Leaves in the Wind',
@ -176,6 +190,7 @@ export const banners = {
color: '#8FFFDE',
featured: ['kaedehara_kazuha'],
featuredRare: ['bennett', 'razor', 'rosaria'],
version: '1.6',
},
{
name: 'The Herons Court',
@ -187,6 +202,7 @@ export const banners = {
featured: ['kamisato_ayaka'],
featuredRare: ['chongyun', 'ningguang', 'yanfei'],
timezoneDependent: true,
version: '2.0',
},
{
name: 'Tapestry of Golden Flames',
@ -197,6 +213,7 @@ export const banners = {
color: '#F7D69F',
featured: ['yoimiya'],
featuredRare: ['sayu', 'diona', 'xinyan'],
version: '2.0',
},
{
name: 'Reign of Serenity',
@ -208,6 +225,7 @@ export const banners = {
featured: ['raiden_shogun'],
featuredRare: ['xiangling', 'sucrose', 'kujou_sara'],
timezoneDependent: true,
version: '2.1',
},
{
name: 'Drifting Luminescence',
@ -218,6 +236,7 @@ export const banners = {
color: '#FF7B69',
featured: ['sangonomiya_kokomi'],
featuredRare: ['rosaria', 'beidou', 'xingqiu'],
version: '2.1',
},
{
name: 'Farewell of Snezhnaya',
@ -229,6 +248,7 @@ export const banners = {
featured: ['tartaglia'],
featuredRare: ['ningguang', 'chongyun', 'yanfei'],
timezoneDependent: true,
version: '2.2',
},
{
name: 'Moment of Bloom',
@ -239,6 +259,7 @@ export const banners = {
color: '#FF7966',
featured: ['hu_tao'],
featuredRare: ['thoma', 'diona', 'sayu'],
version: '2.2',
},
{
name: 'Secretum Secretorum',
@ -250,6 +271,7 @@ export const banners = {
featured: ['albedo', 'eula'],
featuredRare: ['rosaria', 'noelle', 'bennett'],
timezoneDependent: true,
version: '2.3',
},
// {
// name: 'Born of Ocean Swell',
@ -261,6 +283,7 @@ export const banners = {
// featured: ['eula'],
// featuredRare: ['rosaria', 'noelle', 'bennett'],
// timezoneDependent: true,
// version: '2.3'
// },
{
name: "Oni's Royale",
@ -271,6 +294,7 @@ export const banners = {
color: '#FFB455',
featured: ['arataki_itto'],
featuredRare: ['gorou', 'xiangling', 'barbara'],
version: '2.3',
},
{
name: 'The Transcendent One Returns',
@ -282,6 +306,7 @@ export const banners = {
featured: ['shenhe', 'xiao'],
featuredRare: ['yun_jin', 'ningguang', 'chongyun'],
timezoneDependent: true,
version: '2.4',
},
// {
// name: 'Invitation to Mundane Life',
@ -293,6 +318,7 @@ export const banners = {
// featured: ['shenhe', 'xiao'],
// featuredRare: ['yun_jin', 'ningguang', 'chongyun'],
// timezoneDependent: true,
// version: '2.4'
// },
{
name: 'Gentry of Hermitage',
@ -303,6 +329,7 @@ export const banners = {
color: '#FFF5BF',
featured: ['zhongli', 'ganyu'],
featuredRare: ['xingqiu', 'beidou', 'yanfei'],
version: '2.4',
},
// {
// name: 'Adrift in the Harbor',
@ -324,6 +351,7 @@ export const banners = {
featured: ['yae_miko'],
featuredRare: ['thoma', 'diona', 'fischl'],
timezoneDependent: true,
version: '2.5',
},
{
name: 'Reign of Serenity',
@ -334,6 +362,7 @@ export const banners = {
color: '#D0AEF2',
featured: ['raiden_shogun', 'sangonomiya_kokomi'],
featuredRare: ['bennett', 'xinyan', 'kujou_sara'],
version: '2.5',
},
// {
// name: 'Drifting Luminescence',
@ -344,6 +373,7 @@ export const banners = {
// color: '#53caf3',
// featured: ['raiden_shogun', 'sangonomiya_kokomi'],
// featuredRare: ['bennett', 'xinyan', 'kujou_sara'],
// version: '2.5'
// },
{
name: 'Azure Excursion',
@ -355,6 +385,7 @@ export const banners = {
featured: ['kamisato_ayato', 'venti'],
featuredRare: ['yun_jin', 'xiangling', 'sucrose'],
timezoneDependent: true,
version: '2.6',
},
// {
// name: 'Ballad in Goblets',
@ -366,6 +397,7 @@ export const banners = {
// featured: ['kamisato_ayato', 'venti'],
// featuredRare: ['yun_jin', 'xiangling', 'sucrose'],
// timezoneDependent: true,
// version: '2.6'
// },
{
name: 'The Herons Court',
@ -377,6 +409,7 @@ export const banners = {
featured: ['kamisato_ayaka'],
featuredRare: ['sayu', 'razor', 'rosaria'],
timezoneDependentEnd: true,
version: '2.6',
},
{
name: 'Discerner of Enigmas',
@ -388,6 +421,7 @@ export const banners = {
featured: ['yelan', 'xiao'],
featuredRare: ['barbara', 'noelle', 'yanfei'],
timezoneDependent: true,
version: '2.7',
},
// {
// name: 'Invitation to Mundane Life',
@ -409,6 +443,7 @@ export const banners = {
color: '#FFB455',
featured: ['arataki_itto'],
featuredRare: ['chongyun', 'gorou', 'kuki_shinobu'],
version: '2.7',
},
{
name: 'Leaves in the Wind',
@ -420,6 +455,7 @@ export const banners = {
featured: ['kaedehara_kazuha', 'klee'],
featuredRare: ['ningguang', 'thoma', 'shikanoin_heizou'],
timezoneDependent: true,
version: '2.8',
},
// {
// name: "Sparkling Steps",
@ -431,6 +467,7 @@ export const banners = {
// featured: ['kaedehara_kazuha', 'klee'],
// featuredRare: ['ningguang', 'thoma', 'shikanoin_heizou'],
// timezoneDependent: true,
// version: '2.8'
// },
{
name: 'Tapestry of Golden Flames',
@ -441,6 +478,7 @@ export const banners = {
color: '#fc8976',
featured: ['yoimiya'],
featuredRare: ['yun_jin', 'xinyan', 'bennett'],
version: '2.8',
},
{
name: 'Viridescent Vigil',
@ -452,6 +490,7 @@ export const banners = {
featured: ['tighnari', 'zhongli'],
featuredRare: ['collei', 'fischl', 'diona'],
timezoneDependent: true,
version: '3.0',
},
// {
// name: 'Gentry of Hermitage',
@ -463,6 +502,7 @@ export const banners = {
// featured: ['tighnari', 'zhongli'],
// featuredRare: ['collei', 'fischl', 'diona'],
// timezoneDependent: true,
// version: '3.0'
// },
],
weapons: [

View File

@ -10,6 +10,7 @@ export const bannersDual = {
featured: ['albedo', 'eula'],
featuredRare: ['rosaria', 'noelle', 'bennett'],
timezoneDependent: true,
version: '2.3',
},
{
name: 'Born of Ocean Swell',
@ -21,6 +22,7 @@ export const bannersDual = {
featured: ['albedo', 'eula'],
featuredRare: ['rosaria', 'noelle', 'bennett'],
timezoneDependent: true,
version: '2.3',
},
],
'The Transcendent One Returns 1': [
@ -34,6 +36,7 @@ export const bannersDual = {
featured: ['shenhe', 'xiao'],
featuredRare: ['yun_jin', 'ningguang', 'chongyun'],
timezoneDependent: true,
version: '2.4',
},
{
name: 'Invitation to Mundane Life',
@ -45,6 +48,7 @@ export const bannersDual = {
featured: ['shenhe', 'xiao'],
featuredRare: ['yun_jin', 'ningguang', 'chongyun'],
timezoneDependent: true,
version: '2.4',
},
],
'Gentry of Hermitage 3': [
@ -57,6 +61,7 @@ export const bannersDual = {
color: '#FFF5BF',
featured: ['zhongli', 'ganyu'],
featuredRare: ['xingqiu', 'beidou', 'yanfei'],
version: '2.4',
},
{
name: 'Adrift in the Harbor',
@ -67,6 +72,7 @@ export const bannersDual = {
color: '#FFF5BF',
featured: ['zhongli', 'ganyu'],
featuredRare: ['xingqiu', 'beidou', 'yanfei'],
version: '2.4',
},
],
'Reign of Serenity 2': [
@ -79,6 +85,7 @@ export const bannersDual = {
color: '#D0AEF2',
featured: ['raiden_shogun', 'sangonomiya_kokomi'],
featuredRare: ['bennett', 'xinyan', 'kujou_sara'],
version: '2.5',
},
{
name: 'Drifting Luminescence',
@ -89,6 +96,7 @@ export const bannersDual = {
color: '#53caf3',
featured: ['raiden_shogun', 'sangonomiya_kokomi'],
featuredRare: ['bennett', 'xinyan', 'kujou_sara'],
version: '2.5',
},
],
'Azure Excursion 1': [
@ -102,6 +110,7 @@ export const bannersDual = {
featured: ['kamisato_ayato', 'venti'],
featuredRare: ['yun_jin', 'xiangling', 'sucrose'],
timezoneDependent: true,
version: '2.6',
},
{
name: 'Ballad in Goblets',
@ -113,6 +122,7 @@ export const bannersDual = {
featured: ['kamisato_ayato', 'venti'],
featuredRare: ['yun_jin', 'xiangling', 'sucrose'],
timezoneDependent: true,
version: '2.6',
},
],
'Discerner of Enigmas 1': [
@ -126,6 +136,7 @@ export const bannersDual = {
featured: ['yelan', 'xiao'],
featuredRare: ['barbara', 'noelle', 'yanfei'],
timezoneDependent: true,
version: '2.7',
},
{
name: 'Invitation to Mundane Life',
@ -137,6 +148,7 @@ export const bannersDual = {
featured: ['yelan', 'xiao'],
featuredRare: ['barbara', 'noelle', 'yanfei'],
timezoneDependent: true,
version: '2.7',
},
],
'Leaves in the Wind 2': [
@ -150,6 +162,7 @@ export const bannersDual = {
featured: ['kaedehara_kazuha', 'klee'],
featuredRare: ['ningguang', 'thoma', 'shikanoin_heizou'],
timezoneDependent: true,
version: '2.8',
},
{
name: 'Sparkling Steps',
@ -161,6 +174,7 @@ export const bannersDual = {
featured: ['kaedehara_kazuha', 'klee'],
featuredRare: ['ningguang', 'thoma', 'shikanoin_heizou'],
timezoneDependent: true,
version: '2.8',
},
],
'Viridescent Vigil 1': [
@ -174,6 +188,7 @@ export const bannersDual = {
featured: ['tighnari', 'zhongli'],
featuredRare: ['collei', 'fischl', 'diona'],
timezoneDependent: true,
version: '3.0',
},
{
name: 'Gentry of Hermitage',
@ -185,6 +200,7 @@ export const bannersDual = {
featured: ['tighnari', 'zhongli'],
featuredRare: ['collei', 'fischl', 'diona'],
timezoneDependent: true,
version: '3.0',
},
],
};

View File

@ -16,6 +16,7 @@
"fishing": "Fishing",
"radiantSpincrystal": "Radiant Spincrystal",
"calendar": "Calendar",
"banners": "Character Reruns",
"settings": "Settings",
"donate": "Donate"
},
@ -976,6 +977,10 @@
"lastAppearance": "Last Banner Appearance",
"lastAppearanceDesc": "When is the character's last appearance on a limited banner",
"bannerHover": "X Banner Ago",
"birthday": "Birthday"
"birthday": "Birthday",
"sortByTime": "Sorted by first release",
"sortByRerun": "Sorted by oldest re-run",
"bannerTitle": "Character Release Timeline",
"bannerSubtitle": "See when the character is released and their last re-run"
}
}

View File

@ -0,0 +1,229 @@
<script>
import { onMount, tick } from 'svelte';
import { t } from 'svelte-i18n';
import { banners } from '../../data/banners';
import { characters } from '../../data/characters';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
import { mdiSwapVertical } from '@mdi/js';
import Ad from '../../components/Ad.svelte';
let container;
let length = 0;
let versions = [];
let rows = [[]];
let names = [{ name: '', length: 0 }];
let hovered = -1;
let __rows5;
let __rows4;
let __names5;
let __names4;
let sort = false;
async function process() {
length = banners.characters.length;
let _versions = {};
let _chars5 = {};
let _chars4 = {};
let _rows5 = [];
let _rows4 = [];
let _names5 = [];
let _names4 = [];
let pos5 = 0;
let pos4 = 0;
let len = 0;
for (const banner of banners.characters) {
if (_versions[banner.version] === undefined) {
_versions[banner.version] = 0;
}
_versions[banner.version]++;
for (const ch of Object.keys(_chars5)) {
_chars5[ch].length++;
_names5[_chars5[ch].pos].length++;
_rows5[_chars5[ch].pos][len] = { l: _chars5[ch].length, m: 15 };
}
for (const ch of Object.keys(_chars4)) {
_chars4[ch].length++;
_names4[_chars4[ch].pos].length++;
_rows4[_chars4[ch].pos][len] = { l: _chars4[ch].length, m: 9 };
}
for (const char of banner.featured) {
if (_chars5[char] === undefined) {
_chars5[char] = {
pos: pos5,
length: 0,
};
_names5[pos5] = { name: characters[char].name, length: 0 };
_rows5[pos5] = [...new Array(len).fill({ l: '' }), { char, l: 0 }];
pos5++;
} else {
_rows5[_chars5[char].pos][len] = { char, l: 0 };
_names5[_chars5[char].pos].length = 0;
_chars5[char].length = 0;
}
}
for (const char of banner.featuredRare) {
if (_chars4[char] === undefined) {
_chars4[char] = {
pos: pos4,
length: 0,
};
_names4[pos4] = { name: characters[char].name, length: 0 };
_rows4[pos4] = [...new Array(len).fill({ l: '' }), { char, l: 0 }];
pos4++;
} else {
_rows4[_chars4[char].pos][len] = { char, l: 0 };
_names4[_chars4[char].pos].length = 0;
_chars4[char].length = 0;
}
}
len++;
}
versions = Object.entries(_versions).map(([version, length]) => ({
version,
length,
}));
__rows5 = _rows5;
__rows4 = _rows4;
__names5 = _names5;
__names4 = _names4;
rows = [..._rows5, new Array(length).fill({ l: '' }), ..._rows4];
names = [..._names5, { name: '', length: 0 }, ..._names4];
await tick();
container.scrollTo({
left: container.scrollWidth,
top: 0,
behavior: 'smooth',
});
}
function sortOrder() {
sort = !sort;
if (!sort) {
rows = [...__rows5, new Array(length).fill({ l: '' }), ...__rows4];
names = [...__names5, { name: '', length: 0 }, ...__names4];
return;
}
const _rows5 = [...__rows5].sort((a, b) => b[length - 1].l - a[length - 1].l);
const _rows4 = [...__rows4].sort((a, b) => b[length - 1].l - a[length - 1].l);
const _names5 = [...__names5].sort((a, b) => b.length - a.length);
const _names4 = [...__names4].sort((a, b) => b.length - a.length);
console.log(_rows5);
rows = [..._rows5, new Array(length).fill({ l: '' }), ..._rows4];
names = [..._names5, { name: '', length: 0 }, ..._names4];
}
function getColor(index, max) {
if (index === '') return 'none';
const hue = ((max - index) / max) * 100;
return `hsl(${hue}, 60%, 70%)`;
}
function onHover(index) {
hovered = index;
}
onMount(() => {
process();
});
</script>
<svelte:head>
<title>Character Release Timeline - Paimon.moe</title>
<meta name="description" content="Genshin Impact Character Release Timeline" />
<meta property="og:description" content="See when the character is released and their last re-run" />
</svelte:head>
<div class="lg:ml-64 pt-20 lg:pt-8">
<div class="mb-4 pl-8">
<h1 class="font-display font-black text-2xl xl:text-5xl mb-3 xl:mb-0 text-white">{$t('calendar.bannerTitle')}</h1>
<p class="text-gray-400 font-medium pb-4 leading-none" style="margin-top: -1rem;">
{$t('calendar.bannerSubtitle')}
</p>
<div class="flex">
<p class="text-white mr-2 border-2 border-white border-opacity-25 px-4 py-2 rounded-xl">
{$t(sort ? 'calendar.sortByRerun' : 'calendar.sortByTime')}
</p>
<Button size="sm" className="px-2" on:click={sortOrder}><Icon path={mdiSwapVertical} /></Button>
</div>
</div>
<div class="overflow-x-auto whitespace-nowrap px-8 pb-4" bind:this={container}>
<table class="table-fixed">
<tbody>
<tr>
{#each versions as v, index}
<td class="text-center border border-gray-600 text-white font-bold relative" colspan={v.length}
>{v.version}</td
>
{/each}
</tr>
{#each rows as r, rowIndex}
<tr>
{#each r as col, index}
{#if col.char}
<td on:mouseenter={() => onHover(index)} class="cell {hovered === index ? 'hovered' : ''}">
<img class="w-full h-full" src="/images/characters/{col.char}.png" alt={col.char} />
</td>
{:else}
<td
on:mouseenter={() => onHover(index)}
class="cell {hovered === index ? 'hovered' : ''}"
style="background: {getColor(col.l, col.m)};">{col.l}</td
>
{/if}
{/each}
<td class="border border-gray-600 text-white px-2">{$t(names[rowIndex].name)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<Ad type="desktop" variant="lb" id="2" />
<Ad type="mobile" variant="lb" id="1" />
<style lang="postcss">
.cell {
@apply border border-gray-600 w-8 h-8 min-w-[2rem] min-h-[2rem] xl:min-w-[2.5rem] xl:min-h-[2.5rem] xl:w-10 xl:h-10 text-center relative z-0;
}
.hovered::after {
content: '';
@apply absolute bg-white bg-opacity-20 left-0 top-0 h-8 w-8 xl:h-10 xl:w-10;
}
::-webkit-scrollbar {
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
margin: 0 20px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.35);
@apply rounded-xl;
}
</style>

View File

@ -27,6 +27,7 @@
import { getAccountPrefix } from '../../stores/account';
import { readSave } from '../../stores/saveManager';
import { bannersDual } from '../../data/bannersDual';
import Ad from '../../components/Ad.svelte';
const { open: openModal } = getContext('simple-modal');
@ -527,6 +528,8 @@
</div>
</div>
</div>
<Ad type="desktop" variant="lb" id="2" />
<Ad type="mobile" variant="lb" id="1" />
<style lang="postcss">
.event-strip {

View File

@ -8,10 +8,11 @@ const IMAGE_CACHE = `cacheimg${IMAGE_CACHE_VER}`;
const IMAGE_URL = `${self.location.origin}/images/`;
const changelog = [
'Add character release timeline (Database > Character Reruns)',
'Add checklist to achievement',
'Update achievement commission list',
'Update wish import instruction for pc',
'Adjust calendar view on mobile',
'Update timeline',
];
const channel = new BroadcastChannel('paimonmoe-sw');