Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ import './cookie-clicker';
import './balatro';
import './snake';
import './velocity-quest';
import './katamari';

export * from './channels';
Binary file added src/channels/katamari/assets/bgBlocker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions src/channels/katamari/assets/flower.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
198 changes: 198 additions & 0 deletions src/channels/katamari/assets/katamari.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/channels/katamari/assets/landscapes/License.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://craftpix.net/file-licenses/
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/channels/katamari/assets/sprites/prince.png
100 changes: 100 additions & 0 deletions src/channels/katamari/backgrounds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import styled from '@emotion/styled';
import { keyframes } from '@emotion/react';

export type BackgroundLayer = {
src: string;
opacity?: number;
blendMode?: React.CSSProperties['mixBlendMode'];
size?: React.CSSProperties['backgroundSize'];
position?: React.CSSProperties['backgroundPosition'];
repeat?: React.CSSProperties['backgroundRepeat'];
speedSec?: number;
direction?: 'left' | 'right';
distancePx?: number;
};

export type BackgroundScene = {
id: string;
layers: BackgroundLayer[];
};

export type ParallaxBackgroundProps = {
className?: string;
backgrounds?: BackgroundScene[];
selectedIndex?: number;
};

function wrapIndex(idx: number, len: number) {
return ((idx % len) + len) % len;
}

function resolvePreset({ backgrounds, selectedIndex }: ParallaxBackgroundProps): BackgroundScene | undefined {
if (!backgrounds || backgrounds.length === 0) return undefined;

if (typeof selectedIndex === 'number') {
return backgrounds[wrapIndex(selectedIndex, backgrounds.length)];
}

// Default: first preset
return backgrounds[0];
}

const scrollLeft = keyframes`
from { background-position-x: 0; }
to { background-position-x: calc(-1 * var(--bg-dist)); }
`;

const scrollRight = keyframes`
from { background-position-x: 0; }
to { background-position-x: var(--bg-dist); }
`;

export function ParallaxBackground(props: ParallaxBackgroundProps) {
const resolved = resolvePreset(props);
if (!resolved) return null;

return (
<Root className={props.className} data-preset={resolved.id}>
{resolved.layers.slice(0, 9).map((layer, i) => (
<Layer
key={`${resolved.id}:${i}`}
data-dir={layer.direction ?? 'left'}
style={{
backgroundImage: `url(${layer.src})`,
opacity: layer.opacity ?? 1,
mixBlendMode: layer.blendMode ?? 'normal',
backgroundSize: layer.size ?? 'cover',
backgroundPosition: layer.position ?? 'center',
backgroundRepeat: layer.repeat ?? 'no-repeat',
['--bg-dist' as any]: `${layer.distancePx ?? 320}px`,
animationDuration: `${layer.speedSec ?? 24}s`,
zIndex: i,
}}
/>
))}
</Root>
);
}

const Root = styled.div`
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
`;

const Layer = styled.div`
position: absolute;
inset: 0;
will-change: background-position;
animation-timing-function: linear;
animation-iteration-count: infinite;
&[data-dir='left'] {
animation-name: ${scrollLeft};
}
&[data-dir='right'] {
animation-name: ${scrollRight};
}
`;
662 changes: 662 additions & 0 deletions src/channels/katamari/components.tsx

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions src/channels/katamari/donationFlyers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React, { useRef, useState } from 'react';
import styled from '@emotion/styled';
import { clamp01, lerp } from './utilities';
import { useRafLoop } from 'react-use';

export type Flyer = {
id: string;
src: string;
amountText: string;
durationMs: number;
startX: number;
startY: number;
endX: number;
endY: number;
stick: boolean;
arrived?: boolean;
scale: number;
};

export type Stuck = {
id: string;
src: string;
angleDeg: number;
radiusPx: number;
scale?: number;
};

export type DonationFlyersProps = {
flyers: Flyer[];
stuck: Stuck[];
targetX: number;
targetY: number;
onFlyerArrive: (flyer: Flyer, orbitAngleDeg: number) => void;
};

const ORBIT_PERIOD_MS = 1600;

export function DonationFlyers({ flyers, stuck, targetX, targetY, onFlyerArrive }: DonationFlyersProps) {
const [now, setNow] = useState(() => Date.now());
useRafLoop(() => setNow(Date.now()));

const loadedIdsRef = useRef<Set<string>>(new Set());
const startAtRef = useRef<Map<string, number>>(new Map());
const arrivedIdsRef = useRef<Set<string>>(new Set());
const orbitStartMsRef = useRef<number>(Date.now());

const orbitAngleDeg = (((now - orbitStartMsRef.current) % ORBIT_PERIOD_MS) / ORBIT_PERIOD_MS) * 360;

const markLoaded = (id: string) => {
if (!loadedIdsRef.current.has(id)) {
loadedIdsRef.current.add(id);
startAtRef.current.set(id, Date.now());
setNow(Date.now());
}
};

return (
<Layer>
{flyers.map((f) => {
const loaded = loadedIdsRef.current.has(f.id);
const startMs = startAtRef.current.get(f.id);
const t = loaded && startMs != null ? clamp01((now - startMs) / f.durationMs) : 0;

const x = lerp(f.startX, f.endX, t);
const y = lerp(f.startY, f.endY, t);

if (t >= 1 && !arrivedIdsRef.current.has(f.id)) {
arrivedIdsRef.current.add(f.id);
queueMicrotask(() => onFlyerArrive(f, orbitAngleDeg));
}

return (
<FlyerRoot key={f.id} style={{ left: x, top: y, ['--flyer-scale' as any]: f.scale }}>
{!f.stick && (
<AmountText style={{ visibility: f.arrived ? 'hidden' : 'visible' }}>
{/*{f.amountText}*/}
</AmountText>
)}
<FlyerImg
src={f.src}
draggable={false}
style={{ transform: `scale(${f.scale})`, opacity: loaded ? 1 : 0 }}
onLoad={() => markLoaded(f.id)}
onError={() => markLoaded(f.id)}
/>
</FlyerRoot>
);
})}

{stuck.length > 0 && (
<OrbitRoot style={{ left: targetX, top: targetY }}>
<OrbitSpin style={{ transform: `rotate(${orbitAngleDeg}deg)` }}>
{stuck.map((s) => (
<StuckItem
key={s.id}
style={{ transform: `rotate(${s.angleDeg}deg) translate(${s.radiusPx}px)` }}>
<StuckImg
src={s.src}
draggable={false}
style={s.scale != null ? { transform: `scale(${s.scale})` } : undefined}
/>
</StuckItem>
))}
</OrbitSpin>
</OrbitRoot>
)}
</Layer>
);
}

const Layer = styled.div`
position: absolute;
inset: 0;
pointer-events: none;
`;

const FlyerRoot = styled.div`
position: absolute;
transform: translate(-50%, -50%);
display: grid;
place-items: center;
will-change: left, top;
`;

const AmountText = styled.div`
position: absolute;
left: 50%;
top: calc(-10px - (64px * var(--flyer-scale)) / 2);
transform: translateX(-50%);
white-space: nowrap;
font-size: 20px;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
font-family: gdqpixel, serif;
`;

const FlyerImg = styled.img`
width: 64px;
height: 64px;
object-fit: contain;
transition: opacity 120ms linear;
`;

const OrbitRoot = styled.div`
position: absolute;
width: 0;
height: 0;
transform: translate(-50%, -50%);
`;

const OrbitSpin = styled.div`
position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;
will-change: transform;
`;

const StuckItem = styled.div`
position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;
`;

const StuckImg = styled.img`
width: 64px;
height: 64px;
object-fit: contain;
`;
Loading