454 lines
13 KiB
TypeScript
454 lines
13 KiB
TypeScript
import {
|
|
Attributes,
|
|
ComponentChild,
|
|
ComponentChildren,
|
|
JSX,
|
|
VNode,
|
|
toChildArray,
|
|
} from "preact";
|
|
import { render } from "preact-render-to-string";
|
|
import { writeText } from "copy-paste";
|
|
// i know it's out of date; i just want something simple
|
|
import { randomInt } from "https://deno.land/std@0.146.0/node/internal/crypto/random.ts";
|
|
|
|
export function Main({
|
|
children,
|
|
...attributes
|
|
}: {
|
|
children?: ComponentChildren;
|
|
style?: JSX.CSSProperties;
|
|
attributes?: JSX.HTMLAttributes;
|
|
}) {
|
|
return (
|
|
<div {...attributes} id="main">
|
|
{...toChildArray(children)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const HCenter = ({
|
|
children,
|
|
...attrs
|
|
}: {
|
|
children: ComponentChildren;
|
|
attrs?: Attributes;
|
|
}) => (
|
|
<div
|
|
{...attrs}
|
|
class="hcenter"
|
|
style="display: flex; justify-content: center;"
|
|
>
|
|
{...toChildArray(children)}
|
|
</div>
|
|
);
|
|
|
|
export const eggbug_emotions = (attrs: JSX.HTMLAttributes<HTMLImageElement>) =>
|
|
({
|
|
smiling: (
|
|
<img
|
|
class="eggbug"
|
|
src="https://staging.cohostcdn.org/attachment/f33b4285-0455-4128-96b8-117054af40c3/eggbugSquare.png"
|
|
alt="eggbug, smiling"
|
|
{...attrs}
|
|
/>
|
|
),
|
|
frowning: (
|
|
<img
|
|
class="eggbug"
|
|
src="https://static.pyrope.net/eggbug-sad.png"
|
|
alt="eggbug, frowning"
|
|
{...attrs}
|
|
/>
|
|
),
|
|
revengeance: (
|
|
<img
|
|
class="eggbug"
|
|
src="https://static.pyrope.net/eggbug-revengeance.png"
|
|
alt="ULTRA EGGBUG REVENGEANCE, MEGA. THERE IS FIRE. THERE IS TRISCAR. EGGBUG REVENGEANCE. YOU ARE. DIE!"
|
|
{...attrs}
|
|
/>
|
|
),
|
|
jpeg_annihilation: (
|
|
<img
|
|
class="eggbug"
|
|
src="https://static.pyrope.net/eggbug-jpeg-annihilation.gif"
|
|
alt="eggbug dissolving as the image gets more and more jpeg"
|
|
{...attrs}
|
|
/>
|
|
),
|
|
golfball: (
|
|
<img
|
|
class="eggbug"
|
|
src="https://static.pyrope.net/eggbug-golfball.png"
|
|
alt="eggbug as a golfball. no legs"
|
|
{...attrs}
|
|
/>
|
|
),
|
|
} as const);
|
|
|
|
export type EggbugEmotion = keyof ReturnType<typeof eggbug_emotions>;
|
|
type bla = ({
|
|
type: EggbugEmotion;
|
|
} & JSX.HTMLAttributes<HTMLImageElement>)["type"];
|
|
|
|
export const EggbugImg = ({
|
|
type,
|
|
...attrs
|
|
}: { type: EggbugEmotion } & Omit<
|
|
JSX.HTMLAttributes<HTMLImageElement>,
|
|
"type"
|
|
>) => eggbug_emotions(attrs)[type];
|
|
|
|
export const render_and_copy = (elem: VNode, pretty = false) => {
|
|
const rendered = render(elem, null, { pretty });
|
|
writeText(rendered);
|
|
console.log(rendered);
|
|
};
|
|
|
|
export const mk_class_wrapper =
|
|
(klass: string) =>
|
|
({ children }: { children?: ComponentChildren }) =>
|
|
<div class={klass}>{...toChildArray(children)}</div>;
|
|
|
|
export const slidify = (
|
|
Slide: (_: {
|
|
slide: JSX.Element;
|
|
next?: JSX.Element;
|
|
n: number;
|
|
max: number;
|
|
}) => JSX.Element,
|
|
slides: JSX.Element[],
|
|
n = 1,
|
|
max = slides.length
|
|
): JSX.Element =>
|
|
slides[0] ? (
|
|
<Slide
|
|
slide={slides[0]}
|
|
next={slidify(Slide, slides.slice(1), n + 1, max)}
|
|
n={n}
|
|
max={max}
|
|
/>
|
|
) : (
|
|
<></>
|
|
);
|
|
|
|
// https://cohost.org/lexyeevee/post/2107474-css-for-css-baby-3 (wayyy down at the bottom)
|
|
export const DisappearOnClick = ({
|
|
children,
|
|
className,
|
|
}: {
|
|
className?: string;
|
|
children: ComponentChildren;
|
|
}) => (
|
|
<details
|
|
open
|
|
class={`disappearing ${className}`}
|
|
style="position: relative; cursor: pointer;"
|
|
>
|
|
<summary style="list-style: none; position: absolute; inset: 0;"></summary>
|
|
{children}
|
|
</details>
|
|
);
|
|
|
|
/// make sure the margins are all legit
|
|
export const ToggleOnClick = ({
|
|
children: [a, b],
|
|
}: {
|
|
children: [ComponentChild, ComponentChild];
|
|
}) => (
|
|
<details style="position: relative; margin: 0 auto; cursor: pointer;">
|
|
<summary style="list-style: none;">{a}</summary>
|
|
<div style="display: block; position: absolute; top: 0; pointer-events: none;">
|
|
{b}
|
|
</div>
|
|
</details>
|
|
);
|
|
|
|
// TODO: probably doesn't work
|
|
export const ReduceOnClick = ({ children }: { children: VNode<{}>[] }) => {
|
|
if (children.length == 0) return <></>;
|
|
if (children.length == 1) return children[1];
|
|
return (
|
|
<details style="position: relative; margin: 0 auto; cursor: pointer;">
|
|
<summary style="list-style: none;">{children[0]}</summary>
|
|
<ReduceOnClick>{...children.slice(1)}</ReduceOnClick>
|
|
</details>
|
|
);
|
|
};
|
|
|
|
export function shuffle<T>(array: T[]): T[] {
|
|
let currentIndex = array.length,
|
|
randomIndex;
|
|
|
|
// While there remain elements to shuffle.
|
|
while (currentIndex != 0) {
|
|
// Pick a remaining element.
|
|
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
currentIndex--;
|
|
|
|
// And swap it with the current element.
|
|
[array[currentIndex], array[randomIndex]] = [
|
|
array[randomIndex],
|
|
array[currentIndex],
|
|
];
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
// type NOf<N extends number, T, R extends any[] = []> = R["length"] extends N
|
|
// ? R
|
|
// : NOf<N, T, [T, ...R]>;
|
|
|
|
// export function n_of<N extends number, T, R extends any[]>(
|
|
// n: N,
|
|
// x: T
|
|
// ): NOf<N, T, R> {
|
|
// return [];
|
|
// }
|
|
|
|
export function n_of<T>(n: number, x: T): T[] {
|
|
return new Array(n).fill(x);
|
|
}
|
|
|
|
export const static_url = (res: string) => `https://static.pyrope.net/${res}`;
|
|
|
|
export const randirect = (...urls: string[]) =>
|
|
`https://pyrope.net/randirect#${urls.join("::")}`;
|
|
|
|
export const serverside_randirect = (...urls: string[]) =>
|
|
`https://pyrope.net/serverside-randirect?${urls.join("::")}`;
|
|
|
|
// could do some [T, ...T[]] shenanigans for totality but meh
|
|
// kinda bad name
|
|
// unhappy with the randomness so i'm going way overkill lol
|
|
export const pick_random = <T,>(xs: readonly T[]): T =>
|
|
xs[randomInt(xs.length)];
|
|
|
|
export const jitter = (n: number) => n * (Math.random() - 0.5);
|
|
|
|
export const svg_url = (svg: string) => `data:image/svg+xml,${encodeURI(svg)}`;
|
|
|
|
// something higher-level might be worthwhile...
|
|
// could namespace; e.g. css.font.sans_serif
|
|
// could put various beziers
|
|
export const css = {
|
|
url(href: string) {
|
|
return `url('${href}')`;
|
|
},
|
|
|
|
px(n: number) {
|
|
return `${n}px`;
|
|
},
|
|
em(n: number) {
|
|
return `${n}em`;
|
|
},
|
|
rem(n: number) {
|
|
return `${n}rem`;
|
|
},
|
|
|
|
deg(n: number) {
|
|
return `${n}deg`;
|
|
},
|
|
rad(n: number) {
|
|
return `${n}rad`;
|
|
},
|
|
turn(n: number) {
|
|
return `${n}turn`;
|
|
},
|
|
// now i feel like this could be generalizable
|
|
// but js metaprogramming SUCKS
|
|
transform(...ts: string[]) {
|
|
return ts.join(" ");
|
|
},
|
|
rotate(by: string) {
|
|
return `rotate(${by})`;
|
|
},
|
|
translateX(by: string) {
|
|
return `translateX(${by})`;
|
|
},
|
|
translateY(by: string) {
|
|
return `translateY(${by})`;
|
|
},
|
|
translate(x: string, y: string) {
|
|
return `translate(${x}, ${y})`;
|
|
},
|
|
|
|
fontstack(...fonts: string[]) {
|
|
return fonts.join(", ");
|
|
},
|
|
// # ruby i wont you ..
|
|
inline_block: "inline-block",
|
|
block: "block",
|
|
grid: "grid",
|
|
flex: "flex",
|
|
fit_content: "fit-content",
|
|
min_content: "min-content",
|
|
max_content: "max-content",
|
|
} as const;
|
|
|
|
export const Filler = ({ height }: { height: string }) => (
|
|
<div style={`height:${height}`}></div>
|
|
);
|
|
|
|
/// nonfunctional, deno's toplevel await (:D) is generally the best workaround
|
|
export function make_sync_no_matter_the_cost<T>(promise: Promise<T>): T {
|
|
let done = false;
|
|
let out;
|
|
|
|
promise.then(t => {
|
|
out = t;
|
|
done = true;
|
|
});
|
|
|
|
return out as T;
|
|
}
|
|
|
|
// INFINITE CREDIT TO @BLACKLE https://cohost.org/blackle/post/72096-h3-style-text-alig
|
|
export const Cycle = ({
|
|
width_px,
|
|
height_px,
|
|
children,
|
|
style,
|
|
credit = true,
|
|
}: {
|
|
width_px: number;
|
|
height_px: number;
|
|
children: [ComponentChild, ...ComponentChild[]];
|
|
style?: Record<string, string>;
|
|
credit?: boolean;
|
|
}) => (
|
|
<div
|
|
style={{
|
|
width: `${width_px}px`,
|
|
height: `${height_px}px`,
|
|
overflow: "hidden",
|
|
...(credit
|
|
? {
|
|
filter: "url(https://mehbark.github.io/#INFINITE%20CREDIT%20TO%20@BLACKLE)",
|
|
}
|
|
: {}),
|
|
...style,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "inline-flex",
|
|
height: `${height_px}px`,
|
|
paddingRight: `${width_px}px`,
|
|
position: "relative",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
{...children.map((c, i) => (
|
|
<details>
|
|
<summary
|
|
style={{
|
|
position: "absolute",
|
|
top: "0px",
|
|
left: `calc(-${width_px * 100}% + ${
|
|
width_px * width_px + width_px * i
|
|
}px)`,
|
|
// width: `${width_px}px`,
|
|
width: `calc(${
|
|
width_px * (children.length - 1) * 2 +
|
|
width_px -
|
|
width_px * i * 2
|
|
}px)`,
|
|
height: `${height_px}px`,
|
|
listStyle: "none",
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
{c}
|
|
</summary>
|
|
<div
|
|
style={{
|
|
width: `${
|
|
i == children.length - 1
|
|
? children.length - 1
|
|
: 1
|
|
}px`,
|
|
}}
|
|
></div>
|
|
</details>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// yet another banger from @blackle
|
|
// i don't think we need the row height stuff for maximum genericity?
|
|
export const DragResizableImage = ({
|
|
url,
|
|
top,
|
|
left,
|
|
width,
|
|
height,
|
|
indicator = true,
|
|
indicator_opacity = 0.3,
|
|
}: {
|
|
url: string;
|
|
top: number;
|
|
left: number;
|
|
width: number;
|
|
height: number;
|
|
indicator?: boolean;
|
|
indicator_opacity?: number;
|
|
}) => (
|
|
<div style="position: absolute;top: 0px;bottom: 0px;left: 0px;direction: rtl;font-size: 0px;line-height: 0;pointer-events: none;white-space: nowrap;">
|
|
<div style="overflow: visible;width: 1px;height: 1px;display: inline-block;direction: ltr;vertical-align: text-top;position: relative;top: -9px;left: -9px;">
|
|
<div style="position: relative;display: grid;grid-template-rows: 1fr;grid-template-columns: 1fr;">
|
|
<div
|
|
style={`pointer-events: none;background-image: url(${url});background-size: 100% 100%;background-position: center center;background-repeat: no-repeat;grid-row: 1;grid-column: 1;transform: translate(-50%, -50%);`}
|
|
></div>
|
|
<div
|
|
style={`overflow: hidden; resize: both; width: ${width}px;height: ${height}px;min-width: 36px; min-height: 36px; top: 0px; left: 0px; pointer-events: auto; position: relative; grid-area: 1 / 1 / auto / auto; transform: translate(-50%, -50%); clip-path: polygon(calc(100% - 18px) calc(100% - 18px), calc(100% - 18px) 100%, 100% 100%, 100% calc(100% - 18px));`}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
style={`overflow: hidden; resize: both; direction: ltr; display: inline-block; width: ${
|
|
left + 18 + width / 2
|
|
}px;height: ${
|
|
top + 18 + height / 2
|
|
}px; position: relative; opacity: ${indicator_opacity}; background: ${
|
|
indicator
|
|
? 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAATklEQVR4AY3OAQYAMQwF0d7/0q2KrzFGZZZVPPLXxlc/RHIjEyIMxBnWBBAFcNODOaSo8X7C3uU/hY3Q5Bwm6vBkCKTiOU3mWkYkI5KQA4BYG/M4zBNLAAAAAElFTkSuQmCC")'
|
|
: "none"
|
|
} 100% 100% no-repeat; clip-path: polygon(calc(100% - 18px) calc(100% - 18px), calc(100% - 18px) 100%, 100% 100%, 100% calc(100% - 18px)); pointer-events: auto;`}
|
|
></div>
|
|
</div>
|
|
);
|
|
|
|
// don't really love this; do not recommend
|
|
export const string_split_once = (
|
|
str: string,
|
|
on: string
|
|
): [string, string] | undefined => {
|
|
const idx = str.indexOf(on);
|
|
if (idx < 0) return;
|
|
return [str.slice(0, idx), str.slice(idx + on.length)];
|
|
};
|
|
|
|
export const HomestuckSpan = ({
|
|
children,
|
|
color,
|
|
style,
|
|
}: {
|
|
children: ComponentChildren;
|
|
color?: string;
|
|
style?: JSX.CSSProperties;
|
|
attributes?: JSX.HTMLAttributes;
|
|
}) => (
|
|
<span
|
|
style={{
|
|
fontFamily: "'courier-std', courier, monospace",
|
|
fontWeight: "bold",
|
|
...{ color },
|
|
}}
|
|
>
|
|
{...toChildArray(children)}
|
|
</span>
|
|
);
|