Advent of SvelteKit 2022

Christmas tree ornaments

Challenge

Source code

+page.svelte

<script lang="ts">
	import ChristmasTree from './ChristmasTree.svelte';
	import ChristmasLights from './ChristmasLights.svelte';
	import type { PageData } from './$types';
	import ChristmasOrnament from './ChristmasOrnament.svelte';
	import { page } from '$app/stores';
	import { submitReplaceState } from '$lib/util';

	export let data: PageData;

	let form: HTMLFormElement;

	$: alternate = $page.url.searchParams.has('alternate');

	// note: this won't work without JS because there's no submit button
</script>

<h1>Christmas tree ornaments</h1>
<form bind:this={form} on:submit={submitReplaceState}>
	<label
		><input
			type="checkbox"
			name="alternate"
			checked={alternate}
			on:change={() => form.requestSubmit()}
		/> Alternate ornaments</label
	>
</form>
<div class="grid">
	<div>
		{#if alternate}
			<ChristmasTree size={7}>
				<svelte:fragment slot="lights">
					<ChristmasLights />
					<ChristmasLights />
				</svelte:fragment>

				<ChristmasOrnament slot="even" color="green" />
				<ChristmasOrnament slot="odd" color="red" />
			</ChristmasTree>
		{:else}
			<ChristmasTree size={7}>
				<svelte:fragment slot="lights">
					<ChristmasLights />
					<ChristmasLights />
				</svelte:fragment>

				<ChristmasOrnament slot="ornaments" />
			</ChristmasTree>
		{/if}
	</div>
</div>

<style>
	.grid {
		width: 100%;
		display: flex;
		justify-content: center;
		align-items: center;
		margin-bottom: 1rem;
		text-align: center;
		flex-direction: column;
		gap: 1rem;
	}

	h2 {
		margin-bottom: 1rem;
	}
</style>

ChristmasLights.svelte

<script lang="ts">
	import type { PRNG } from 'seedrandom';
	import { getContext } from 'svelte';
	let random = getContext<PRNG>('rng');

	// Not important for the challenge, but some math fun:
	// Math.random() gives us in the range [0...1] so we multiply
	// so we can get a range [0...AMPLIFY].
	// BUT! We want our lights centered in the tree still, so we
	// subtract (AMPLIFY / 2) so that we're centered over 0
	// and our range is now [(-AMPLIFY / 2)...(AMPLIFY / 2)]
	const AMPLIFY = 15;
	const offsetY = random() * AMPLIFY - AMPLIFY / 2;
	const offsetX = random() * AMPLIFY - AMPLIFY / 2;
	// Should be the same as the length of the animation so that
	// it's evenly distributed
	const MAX_DELAY = 2;
	const twinkleDelay = random() * MAX_DELAY;
</script>

<div
	style:top="{offsetY}px"
	style:left="{offsetX}px"
	style:animation-delay="{twinkleDelay}s"
	class="twinkle"
/>

<style>
	.twinkle {
		animation: twinkle 2s ease-in-out infinite;
		border-radius: var(--radius-round);
		background: var(--yellow-1);
		width: 0.5rem;
		height: 0.5rem;
		position: relative;
		z-index: 20;
		box-shadow: var(--shadow-3);
	}
	@keyframes twinkle {
		0% {
			opacity: 0;
		}
		50% {
			opacity: 1;
		}
		100% {
			opacity: 0;
		}
	}
</style>

ChristmasOrnament.svelte

<script lang="ts">
	import type { PRNG } from 'seedrandom';
	import { getContext } from 'svelte';
	let random = getContext<PRNG>('rng');

	const AMPLIFY = 20;
	const offsetY = 16 + (random() * AMPLIFY - AMPLIFY / 2);
	const offsetX = 16 + (random() * AMPLIFY - AMPLIFY / 2);

	export let color: 'green' | 'red' = 'red';
</script>

<div
	style:top="{offsetY}px"
	style:left="{offsetX}px"
	style:background-color={color === 'green' ? 'var(--green-3)' : 'var(--red-6)'}
/>

<style>
	div {
		position: absolute;
		border-radius: var(--radius-round);
		width: 1.5rem;
		height: 1.5rem;
		z-index: 10;
		background-color: var(--green);
	}
</style>

ChristmasTree.svelte

<script lang="ts">
	import { fade } from 'svelte/transition';
	export let size = 1;
</script>

{#if size > 1}
	<!-- Workaround for https://github.com/sveltejs/svelte/issues/5604 -->
	{#if $$slots.even}
		<svelte:self size={size - 1}>
			<!-- This tripped me up - combining slot= and name= -->
			<slot slot="lights" name="lights" />
			<slot slot="ornaments" name="ornaments" />
			<slot slot="even" name="even" />
			<slot slot="odd" name="odd" />
		</svelte:self>
	{:else}
		<svelte:self size={size - 1}>
			<slot slot="lights" name="lights" />
			<slot slot="ornaments" name="ornaments" />
		</svelte:self>
	{/if}
{/if}
<div class="tree" in:fade={{ delay: size * 100 }}>
	{#each { length: size } as _, idx}
		<div class="leaf">
			<slot name="lights" />
			{#if idx % 2 === 0}
				<slot name="even">
					<slot name="ornaments" />
				</slot>
			{:else}
				<slot name="odd">
					<slot name="ornaments" />
				</slot>
			{/if}
		</div>
	{/each}
</div>

<style>
	.tree {
		display: flex;
		flex-direction: row;
		justify-content: center;
	}

	.leaf {
		position: relative;
		border-radius: var(--radius-round);
		background: var(--green-6);
		width: var(--size-9);
		height: var(--size-9);
		margin: -0.5rem;
		display: flex;
		justify-content: center;
		place-items: center;
	}
</style>