add settings sidebar & initial templating support 🎉 (#28)
This commit is contained in:
parent
3845756c36
commit
d262be82dd
|
@ -2,13 +2,19 @@ import { Header } from "./Header";
|
||||||
import { AppContext } from "../contexts/AppContext";
|
import { AppContext } from "../contexts/AppContext";
|
||||||
import { CanvasWrapper } from "./CanvasWrapper";
|
import { CanvasWrapper } from "./CanvasWrapper";
|
||||||
import { Pallete } from "./Pallete";
|
import { Pallete } from "./Pallete";
|
||||||
|
import { TemplateContext } from "../contexts/TemplateContext";
|
||||||
|
import { SettingsSidebar } from "./Settings/SettingsSidebar";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<AppContext>
|
<AppContext>
|
||||||
<Header />
|
<TemplateContext>
|
||||||
<CanvasWrapper />
|
<Header />
|
||||||
<Pallete />
|
<CanvasWrapper />
|
||||||
|
<Pallete />
|
||||||
|
|
||||||
|
<SettingsSidebar />
|
||||||
|
</TemplateContext>
|
||||||
</AppContext>
|
</AppContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,12 +7,14 @@ import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { Routes } from "../lib/routes";
|
import { Routes } from "../lib/routes";
|
||||||
import { ICanvasPosition, IPosition } from "@sc07-canvas/lib/src/net";
|
import { ICanvasPosition, IPosition } from "@sc07-canvas/lib/src/net";
|
||||||
|
import { Template } from "./Template";
|
||||||
|
|
||||||
export const CanvasWrapper = () => {
|
export const CanvasWrapper = () => {
|
||||||
// to prevent safari from blurring things, use the zoom css property
|
// to prevent safari from blurring things, use the zoom css property
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<PanZoomWrapper>
|
<PanZoomWrapper>
|
||||||
|
<Template />
|
||||||
<CanvasInner />
|
<CanvasInner />
|
||||||
</PanZoomWrapper>
|
</PanZoomWrapper>
|
||||||
</main>
|
</main>
|
||||||
|
@ -90,7 +92,7 @@ const CanvasInner = () => {
|
||||||
|
|
||||||
setCanvasPosition(canvasPosition);
|
setCanvasPosition(canvasPosition);
|
||||||
|
|
||||||
window.location.replace(Routes.canvas(canvasPosition));
|
window.location.replace(Routes.canvas({ pos: canvasPosition }));
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const handleCursorPos = throttle((pos: IPosition) => {
|
const handleCursorPos = throttle((pos: IPosition) => {
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
|
import { Button } from "@nextui-org/react";
|
||||||
|
import { useAppContext } from "../contexts/AppContext";
|
||||||
import { User } from "./Header/User";
|
import { User } from "./Header/User";
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
|
const { setSettingsSidebar } = useAppContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header id="main-header">
|
<header id="main-header">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="spacer"></div>
|
<div className="spacer"></div>
|
||||||
<div className="box">
|
<div className="box">
|
||||||
<User />
|
<User />
|
||||||
|
<Button onClick={() => setSettingsSidebar(true)}>Settings</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Button } from "@nextui-org/react";
|
||||||
|
import { useAppContext } from "../../contexts/AppContext";
|
||||||
|
import { TemplateSettings } from "./TemplateSettings";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
|
||||||
|
|
||||||
|
export const SettingsSidebar = () => {
|
||||||
|
const { settingsSidebar, setSettingsSidebar } = useAppContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="sidebar sidebar-right"
|
||||||
|
style={{ ...(settingsSidebar ? {} : { display: "none" }) }}
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<div className="flex-grow" />
|
||||||
|
<Button size="sm" isIconOnly onClick={() => setSettingsSidebar(false)}>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
<section>abc</section>
|
||||||
|
<TemplateSettings />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { useTemplateContext } from "../../contexts/TemplateContext";
|
||||||
|
import { Input, Slider, Switch } from "@nextui-org/react";
|
||||||
|
|
||||||
|
export const TemplateSettings = () => {
|
||||||
|
const {
|
||||||
|
enable,
|
||||||
|
setEnable,
|
||||||
|
url,
|
||||||
|
setURL,
|
||||||
|
width,
|
||||||
|
setWidth,
|
||||||
|
x,
|
||||||
|
setX,
|
||||||
|
y,
|
||||||
|
setY,
|
||||||
|
opacity,
|
||||||
|
setOpacity,
|
||||||
|
} = useTemplateContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header>
|
||||||
|
<Switch size="sm" isSelected={enable} onValueChange={setEnable} />
|
||||||
|
<h2>Template</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<Input
|
||||||
|
label="Template URL"
|
||||||
|
size="sm"
|
||||||
|
value={url}
|
||||||
|
onValueChange={setURL}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Template Width"
|
||||||
|
size="sm"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max={10_000}
|
||||||
|
value={width?.toString()}
|
||||||
|
onValueChange={(v) => setWidth(parseInt(v))}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row gap-1">
|
||||||
|
<Input
|
||||||
|
label="Template X"
|
||||||
|
size="sm"
|
||||||
|
type="number"
|
||||||
|
value={x.toString()}
|
||||||
|
onValueChange={(v) => setX(parseInt(v))}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Template Y"
|
||||||
|
size="sm"
|
||||||
|
type="number"
|
||||||
|
value={y.toString()}
|
||||||
|
onValueChange={(v) => setY(parseInt(v))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
label="Template Opacity"
|
||||||
|
step={1}
|
||||||
|
minValue={0}
|
||||||
|
maxValue={100}
|
||||||
|
value={opacity}
|
||||||
|
onChange={(v) => setOpacity(v as number)}
|
||||||
|
getValue={(v) => v + "%"}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
#template {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 100%;
|
||||||
|
// height: 100%;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { Template as TemplateCl } from "../lib/template";
|
||||||
|
import { useAppContext } from "../contexts/AppContext";
|
||||||
|
import { useTemplateContext } from "../contexts/TemplateContext";
|
||||||
|
|
||||||
|
export const Template = () => {
|
||||||
|
const { config } = useAppContext();
|
||||||
|
const { enable, url, width, setWidth, x, y, opacity } = useTemplateContext();
|
||||||
|
const templateHolder = useRef<HTMLDivElement>(null);
|
||||||
|
const instance = useRef<TemplateCl>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!templateHolder?.current) {
|
||||||
|
console.warn("No templateHolder, cannot initialize");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.current = new TemplateCl(config, templateHolder.current);
|
||||||
|
|
||||||
|
instance.current.on("autoDetectWidth", (width) => {
|
||||||
|
console.log("autodetectwidth", width);
|
||||||
|
setWidth(width);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
instance.current?.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!instance.current) {
|
||||||
|
console.warn("Received template enable but no instance exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.current.setOption("enable", enable);
|
||||||
|
|
||||||
|
if (enable && url) {
|
||||||
|
instance.current.loadImage(url).then(() => {
|
||||||
|
console.log("enable: load image finished");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [enable]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!instance.current) {
|
||||||
|
console.warn(
|
||||||
|
"recieved template url update but no template instance exists"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.warn("received template url blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enable) {
|
||||||
|
console.info("Got template URL but not enabled, ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.current.loadImage(url).then(() => {
|
||||||
|
console.log("template loader finished");
|
||||||
|
});
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!instance.current) {
|
||||||
|
console.warn("received template width with no instance");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.current.setOption("width", width);
|
||||||
|
instance.current.rasterizeTemplate();
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="template"
|
||||||
|
ref={templateHolder}
|
||||||
|
style={{
|
||||||
|
top: y,
|
||||||
|
left: x,
|
||||||
|
opacity: opacity / 100,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -26,6 +26,9 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
||||||
|
|
||||||
const [pixels, setPixels] = useState({ available: 0 });
|
const [pixels, setPixels] = useState({ available: 0 });
|
||||||
|
|
||||||
|
// overlays visible
|
||||||
|
const [settingsSidebar, setSettingsSidebar] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleConfig(config: ClientConfig) {
|
function handleConfig(config: ClientConfig) {
|
||||||
console.info("Server sent config", config);
|
console.info("Server sent config", config);
|
||||||
|
@ -64,6 +67,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
setCursorPosition,
|
setCursorPosition,
|
||||||
pixels,
|
pixels,
|
||||||
|
settingsSidebar,
|
||||||
|
setSettingsSidebar,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config ? children : "Loading..."}
|
{config ? children : "Loading..."}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { PropsWithChildren, createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
|
interface ITemplate {
|
||||||
|
/**
|
||||||
|
* If the template is being used
|
||||||
|
*/
|
||||||
|
enable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL of the template image being used
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Width of the template being displayed
|
||||||
|
*
|
||||||
|
* @default min(template.width,canvas.width)
|
||||||
|
*/
|
||||||
|
width?: number;
|
||||||
|
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
opacity: number;
|
||||||
|
|
||||||
|
setEnable(v: boolean): void;
|
||||||
|
setURL(v?: string): void;
|
||||||
|
setWidth(v?: number): void;
|
||||||
|
setX(v: number): void;
|
||||||
|
setY(v: number): void;
|
||||||
|
setOpacity(v: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateContext = createContext<ITemplate>({} as any);
|
||||||
|
|
||||||
|
export const useTemplateContext = () => useContext(templateContext);
|
||||||
|
|
||||||
|
export const TemplateContext = ({ children }: PropsWithChildren) => {
|
||||||
|
const [enable, setEnable] = useState(false);
|
||||||
|
const [url, setURL] = useState<string>();
|
||||||
|
const [width, setWidth] = useState<number>();
|
||||||
|
const [x, setX] = useState(0);
|
||||||
|
const [y, setY] = useState(0);
|
||||||
|
const [opacity, setOpacity] = useState(100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<templateContext.Provider
|
||||||
|
value={{
|
||||||
|
enable,
|
||||||
|
setEnable,
|
||||||
|
url,
|
||||||
|
setURL,
|
||||||
|
width,
|
||||||
|
setWidth,
|
||||||
|
x,
|
||||||
|
setX,
|
||||||
|
y,
|
||||||
|
setY,
|
||||||
|
opacity,
|
||||||
|
setOpacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</templateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,11 +1,37 @@
|
||||||
import { ICanvasPosition } from "../types";
|
import { ICanvasPosition } from "@sc07-canvas/lib/src/net";
|
||||||
|
|
||||||
|
export interface ITemplateState {
|
||||||
|
url: string;
|
||||||
|
width: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
opacity: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const Routes = {
|
export const Routes = {
|
||||||
canvas: (pos: ICanvasPosition) => {
|
canvas: ({
|
||||||
|
pos,
|
||||||
|
template,
|
||||||
|
}: {
|
||||||
|
pos?: ICanvasPosition;
|
||||||
|
template?: ITemplateState;
|
||||||
|
}) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set("x", pos.x + "");
|
|
||||||
params.set("y", pos.y + "");
|
if (pos) {
|
||||||
params.set("zoom", pos.zoom + "");
|
params.set("x", pos.x + "");
|
||||||
|
params.set("y", pos.y + "");
|
||||||
|
params.set("zoom", pos.zoom + "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
let { url, width, x, y, opacity } = template;
|
||||||
|
params.set("template.url", url);
|
||||||
|
params.set("template.width", width + "");
|
||||||
|
params.set("template.x", x + "");
|
||||||
|
params.set("template.y", y + "");
|
||||||
|
params.set("template.opacity", opacity + "");
|
||||||
|
}
|
||||||
|
|
||||||
return "/#" + params;
|
return "/#" + params;
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,570 @@
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
import { WebGLUtils } from "./webgl";
|
||||||
|
import { ClientConfig } from "@sc07-canvas/lib/src/net";
|
||||||
|
|
||||||
|
interface TemplateEvents {
|
||||||
|
updateImageURL(url: string | undefined): void;
|
||||||
|
option<T extends keyof ITemplateOptions>(
|
||||||
|
option: T,
|
||||||
|
value: ITemplateOptions[T]
|
||||||
|
): void;
|
||||||
|
autoDetectWidth(width: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITemplateOptions {
|
||||||
|
enable: boolean;
|
||||||
|
width?: number;
|
||||||
|
style: TemplateStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TemplateStyle {
|
||||||
|
SOURCE = "",
|
||||||
|
ONE_TO_ONE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGklEQVQoz2P8//8/AymAiYFEMKphVMPQ0QAAVW0DHZ8uFaIAAAAASUVORK5CYII=",
|
||||||
|
ONE_TO_ONE_INCORRECT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGklEQVQoz2P8//8/AymAiYFEMKphVMPQ0QAAVW0DHZ8uFaIAAAAASUVORK5CYII=",
|
||||||
|
DOTTED_SMALL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAAAAmJLR0QA/4ePzL8AAAAzSURBVFjD7dBBDQAACMSw828aVEAI6R4T0GShGv6DECFChAgRIkSIECFChAgRIkSIruA0nub+AuTzLZoAAAAASUVORK5CYII=",
|
||||||
|
DOTTED_BIG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAWklEQVR42u3UwQkAIAwEwcX+e9aP2INkBvK4d2CLqva9cXv5PWgAoAGgARoAGqABoAEaABqgAaABGgAaoAGgARoAGqABoAEaABqgAaABGgAaoAGgAT/vRwOmO8dS/DI1VxCbAAAAAElFTkSuQmCC",
|
||||||
|
SYMBOLS = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABwAQMAAAD8LmYIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAZQTFRFAAAAAwMDFQUF7wAAAAJ0Uk5TAP9bkSK1AAAAuUlEQVR4nGNgQAUhjQvdz0uwMfx82OrVIgPkBj/xaOEQ6GRuP9vHAeQGsPjzyVj8LH5+iAXEDTziMd+uplHg+VE+GQaNjwHt5+WB3A+HO+bbMRACDoed+Xg0FIMW97dIMLAwNC45OF8ip+Dh8aN8Ngwsjc2sXfNFBAoePz8xX46B5+DhNj4WlpwCx+NH5W0Yan5+fn6+xU5DwWlxf58EAWs0DFC4NQX4uBaoXAFUvaNgFIyCUTAKaAYAzI49GM5w0hQAAAAOZVhJZk1NACoAAAAIAAAAAAAAANJTkwAAAABJRU5ErkJggg==",
|
||||||
|
NUMBERS = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALAAAACwCAYAAACvt+ReAAAE10lEQVR42u3d3bKjKBQGULDy/q/MXE1VdzqRH9kqZq2bqZn5JKAmErbm5JRSSbCw0pGTlb1VdttpoAS9AUpANgW1u0q2dGxTOo7faLu92TTa7vZlozz55P2/zRyQ7RXRj97+zsyWjm1GT97aGI9kR8aWaydwxIEg7g05ms0nvcnCsq8bzrVnvVNr2RyQzReP7eeO33bBp0We/E6NnJr0nJAR7UZOpR5x/LYEC9smrCyMrETMXErpvazd4fI9c3+VnW/2teze8Ss7qwAt7ZYJ50y13deqk/fBbVYb28iY8mLZvf9ebTcnlTgeOIWAZShJyi6bfX3YOH84sfOXF7oyW3amQrXs++vMarc3m7/048w+rJT957htlU/i3HCQ93J77R7N5o4vD+/ZUvmSkRvHdiSbOvqwt/2RbA7av6cdt+0Bqw8jlMDX9M9xq5WS71xKjS5VtmxbDvZ3JJsDsvnEsU09dq+GM75MPnl72s2VQZx1JehdA23pb8/YevdDax/KhWMrM84Vy2gs7dOXuJGSZMslYLTUWbsUtbT7nm25ibqlhPqp3Z7+po7+RuyHnj707t/S8fql8/XLyHzE2qPs7bJKyTxmCgFLcimSXTa7fdiwfPn3NDGbgtq9ezYNZke++JaAbApqdzj75zrw+9rd3lrekeye1vsljmZ7+5snZL/1q2clJw3uwxnZXlGPWP3VX3PgNSh9f/HaeaeXzk+FEpzNAdl88dhSQPanjttWeafX7lZq/ZRovQPqSLanDyWo3ci70XqyvXeutbQbeVdez91onkrmmVOII3c1RV02I+8Ei2g36sc/SuOVo+WSfKS/EdOfw/2wnii7bFYpmaWZA7M8lyLZZbOvD0sUf/4z7XyJ68n++f88PfyDTw9H9WHWI0W17JFHXmqv+WnHzcymjj7Utj2yvpwC9u/yx+3uc2Al1DWddtxelfnw7DJjxI9Kt14pSuM7flY2B2TzxWO73XF7/12IM8qMtXeuEmpDCfWEsR2dSvVOu4ZuWbCMxtJaf9gkHcjNKM3WVgBqlzGl7/7+HhlfrfQ9ejdaOXqSysreKquUzNLMgVmeS5Hsstlv9wMroY5lW7+4KH1Pyr6vQiihHnsquTSMy1Pf4/v3n6w58FxK3yf7VkpWQo35M7Ol4xPzvd0SnM0B2Rw9tq1y+f7Fp4fPOHlr/SgdYysHxta7H3pOyIh2/a1kfmMK0fqJ0rrd3Uq5nh6O3Q8peP8Obywre6usUjJLMwdmeS5Fsstma6Xkb8scSqjPyC5/3Fp+nfKbI0+hRq0vp45s72MsOaC/V2eXP26z5sBKqGta/rjNWgfuyfrh7Pix/cxx2w68Iy95CvWiS5wfzt7f/rKnvi2j8egpxC2fQr355TCiXU9972xrPVF22axSMo+aQkCUsCU7lyLZM7Lhn8BKqOf39xdL31PN+kOHSqhj+yF1ju0ppe+wE9h8jKW/xK1WQj1D5GM3I9mIH5vOF49tyifwij/AfOYndk8JNqLNiDJ/CWr3tOOmlMxjphB+gPn4VErp+4Jpn3VK2TOyYXM7pWTO+h4BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE/1H4IIqRgL4W2oAAAAAElFTkSuQmCC",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Template extends EventEmitter<TemplateEvents> {
|
||||||
|
config: ClientConfig;
|
||||||
|
|
||||||
|
$wrapper: HTMLDivElement;
|
||||||
|
$imageLoader: HTMLImageElement;
|
||||||
|
$style: HTMLImageElement;
|
||||||
|
$canvas: HTMLCanvasElement;
|
||||||
|
imageURL: string | undefined;
|
||||||
|
|
||||||
|
options: ITemplateOptions = {
|
||||||
|
enable: false,
|
||||||
|
style: TemplateStyle.ONE_TO_ONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(config: ClientConfig, templateHolder: HTMLDivElement) {
|
||||||
|
super();
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
console.log("template init", config, templateHolder);
|
||||||
|
|
||||||
|
this.$wrapper = templateHolder;
|
||||||
|
|
||||||
|
this.$imageLoader = document.createElement("img");
|
||||||
|
this.$imageLoader.setAttribute("crossorigin", "");
|
||||||
|
this.$imageLoader.addEventListener("load", () => {
|
||||||
|
console.log("imageLoader loaded image");
|
||||||
|
if (!this.options.width) {
|
||||||
|
this.setOption("width", this.$imageLoader.naturalWidth);
|
||||||
|
this.emit("autoDetectWidth", this.$imageLoader.naturalWidth);
|
||||||
|
}
|
||||||
|
this.rasterizeTemplate();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$style = document.createElement("img");
|
||||||
|
this.$style.setAttribute("crossorigin", "");
|
||||||
|
this.$style.setAttribute("src", this.options!.style);
|
||||||
|
|
||||||
|
this.$canvas = document.createElement("canvas");
|
||||||
|
|
||||||
|
[this.$imageLoader, this.$style, this.$canvas].forEach((el) =>
|
||||||
|
el.classList.add("pixelate")
|
||||||
|
);
|
||||||
|
|
||||||
|
templateHolder.style.width = this.options!.width + "px";
|
||||||
|
templateHolder.appendChild(this.$imageLoader);
|
||||||
|
// templateHolder.appendChild(this.$style);
|
||||||
|
templateHolder.appendChild(this.$canvas);
|
||||||
|
|
||||||
|
this.setupWebGL();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.TemplateTest = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.$imageLoader.remove();
|
||||||
|
this.$style.remove();
|
||||||
|
this.$canvas.remove();
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update settings
|
||||||
|
*
|
||||||
|
* NOTE: this does not cause re-render
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
setOption<T extends keyof ITemplateOptions>(
|
||||||
|
key: T,
|
||||||
|
value: ITemplateOptions[T]
|
||||||
|
) {
|
||||||
|
this.options[key] = value;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "enable":
|
||||||
|
this.setElementVisible([this.$canvas, this.$imageLoader], !!value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit("option", key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setElementVisible(els: HTMLElement[], visible: boolean) {
|
||||||
|
for (const el of els) {
|
||||||
|
el.style.display = visible ? "block" : "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rasterizeTemplate() {
|
||||||
|
this.downscaleTemplate();
|
||||||
|
this.stylizeTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadImage(url: string) {
|
||||||
|
return fetch(url, { method: "GET", credentials: "omit" })
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
return resp.blob();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((blob) => {
|
||||||
|
return new Promise<void>((res) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
// reader.result will be a string because of reader.readAsDataURL being called
|
||||||
|
this.imageURL = reader.result as string;
|
||||||
|
this.$imageLoader.setAttribute("src", this.imageURL);
|
||||||
|
this.emit("updateImageURL", this.imageURL);
|
||||||
|
res();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// TODO: Better error handling
|
||||||
|
alert("template loadimage error: " + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDimentions() {
|
||||||
|
let source = {
|
||||||
|
width: this.$imageLoader.naturalWidth,
|
||||||
|
height: this.$imageLoader.naturalHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
let aspectRatio = source.height / source.width;
|
||||||
|
|
||||||
|
return {
|
||||||
|
source,
|
||||||
|
display: {
|
||||||
|
width: Math.round(this.options?.width || source.width),
|
||||||
|
height: Math.round((this.options?.width || source.width) * aspectRatio),
|
||||||
|
},
|
||||||
|
aspectRatio,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private context: WebGLRenderingContext | undefined;
|
||||||
|
|
||||||
|
private textures: {
|
||||||
|
source: WebGLTexture | null;
|
||||||
|
downscaled: WebGLTexture | null;
|
||||||
|
style: WebGLTexture | null;
|
||||||
|
} = {} as any;
|
||||||
|
|
||||||
|
private framebuffers: {
|
||||||
|
intermediate: WebGLFramebuffer | null;
|
||||||
|
main: WebGLFramebuffer | null;
|
||||||
|
} = {} as any;
|
||||||
|
private buffers: { vertex: WebGLBuffer | null } = {} as any;
|
||||||
|
private programs: {
|
||||||
|
downscaling: { unconverted: WebGLProgram; nearestCustom: WebGLProgram };
|
||||||
|
stylize: WebGLProgram;
|
||||||
|
} = { downscaling: {} } as any;
|
||||||
|
|
||||||
|
updateSize() {
|
||||||
|
const {
|
||||||
|
display: { width, height },
|
||||||
|
} = this.getDimentions();
|
||||||
|
|
||||||
|
this.$wrapper.style.width = width + "px";
|
||||||
|
this.$imageLoader.style.width = width + "px";
|
||||||
|
|
||||||
|
this.$canvas.width = width;
|
||||||
|
this.$canvas.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize webgl
|
||||||
|
*
|
||||||
|
* This originates from Pxls and their contributors
|
||||||
|
* https://github.com/pxlsspace/pxls-web/blob/0c5a680c4611af277205886df77ac9014c759aba/public/include/template.js#L616
|
||||||
|
*/
|
||||||
|
setupWebGL() {
|
||||||
|
const palette: { value: string }[] = this.config.pallete.colors.map(
|
||||||
|
(color) => ({ value: color.hex })
|
||||||
|
);
|
||||||
|
const STYLES_Y = 16;
|
||||||
|
const STYLES_X = 16;
|
||||||
|
|
||||||
|
const context = this.$canvas.getContext("webgl", {
|
||||||
|
premultipliedAlpha: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error("WebGL is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
const utils = new WebGLUtils(context);
|
||||||
|
|
||||||
|
context.clearColor(0, 0, 0, 0);
|
||||||
|
context.pixelStorei(context.UNPACK_FLIP_Y_WEBGL, true);
|
||||||
|
|
||||||
|
this.textures.source = utils.createTexture();
|
||||||
|
this.textures.downscaled = utils.createTexture();
|
||||||
|
this.framebuffers.intermediate = context.createFramebuffer();
|
||||||
|
|
||||||
|
context.bindFramebuffer(
|
||||||
|
context.FRAMEBUFFER,
|
||||||
|
this.framebuffers.intermediate
|
||||||
|
);
|
||||||
|
context.framebufferTexture2D(
|
||||||
|
context.FRAMEBUFFER,
|
||||||
|
context.COLOR_ATTACHMENT0,
|
||||||
|
context.TEXTURE_2D,
|
||||||
|
this.textures.downscaled,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
this.textures.style = utils.createTexture();
|
||||||
|
this.loadStyle(false);
|
||||||
|
this.buffers.vertex = context.createBuffer();
|
||||||
|
context.bindBuffer(context.ARRAY_BUFFER, this.buffers.vertex);
|
||||||
|
// prettier-ignore
|
||||||
|
context.bufferData(
|
||||||
|
context.ARRAY_BUFFER,
|
||||||
|
new Float32Array([
|
||||||
|
-1, -1,
|
||||||
|
-1, 1,
|
||||||
|
1, -1,
|
||||||
|
1, 1
|
||||||
|
]),
|
||||||
|
context.STATIC_DRAW
|
||||||
|
);
|
||||||
|
|
||||||
|
const identityVertexShader = `
|
||||||
|
attribute vec2 a_Pos;
|
||||||
|
varying vec2 v_TexCoord;
|
||||||
|
void main() {
|
||||||
|
v_TexCoord = a_Pos * vec2(0.5, 0.5) + vec2(0.5, 0.5);
|
||||||
|
gl_Position = vec4(a_Pos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const paletteDefs = `
|
||||||
|
#define PALETTE_LENGTH ${palette.length}
|
||||||
|
#define PALETTE_MAXSIZE 255.0
|
||||||
|
#define PALETTE_TRANSPARENT (PALETTE_MAXSIZE - 1.0) / PALETTE_MAXSIZE
|
||||||
|
#define PALETTE_UNKNOWN 1.0
|
||||||
|
`;
|
||||||
|
const diffCustom = `
|
||||||
|
#define LUMA_WEIGHTS vec3(0.299, 0.587, 0.114)
|
||||||
|
// a simple custom colorspace that stores:
|
||||||
|
// - brightness
|
||||||
|
// - red/green-ness
|
||||||
|
// - blue/yellow-ness
|
||||||
|
// this storing of contrasts is similar to how humans
|
||||||
|
// see color difference and provides a simple difference function
|
||||||
|
// with decent results.
|
||||||
|
vec3 rgb2Custom(vec3 rgb) {
|
||||||
|
return vec3(
|
||||||
|
length(rgb * LUMA_WEIGHTS),
|
||||||
|
rgb.r - rgb.g,
|
||||||
|
rgb.b - (rgb.r + rgb.g) / 2.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
float diffCustom(vec3 col1, vec3 col2) {
|
||||||
|
return length(rgb2Custom(col1) - rgb2Custom(col2));
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const downscalingFragmentShader = (comparisonFunctionName?: string) => `
|
||||||
|
precision mediump float;
|
||||||
|
// GLES (and thus WebGL) does not support dynamic for loops
|
||||||
|
// the workaround is to specify the condition as an upper bound
|
||||||
|
// then break the loop early if we reach our dynamic limit
|
||||||
|
#define MAX_SAMPLE_SIZE 16.0
|
||||||
|
|
||||||
|
${paletteDefs}
|
||||||
|
${comparisonFunctionName !== undefined ? "#define CONVERT_COLORS" : ""}
|
||||||
|
#define HIGHEST_DIFF 999999.9
|
||||||
|
uniform sampler2D u_Template;
|
||||||
|
uniform vec2 u_TexelSize;
|
||||||
|
uniform vec2 u_SampleSize;
|
||||||
|
uniform vec3 u_Palette[PALETTE_LENGTH];
|
||||||
|
varying vec2 v_TexCoord;
|
||||||
|
const float epsilon = 1.0 / 128.0;
|
||||||
|
// The alpha channel is used to index the palette:
|
||||||
|
const vec4 transparentColor = vec4(0.0, 0.0, 0.0, PALETTE_TRANSPARENT);
|
||||||
|
${diffCustom}
|
||||||
|
void main () {
|
||||||
|
vec4 color = vec4(0.0);
|
||||||
|
vec2 actualSampleSize = min(u_SampleSize, vec2(MAX_SAMPLE_SIZE));
|
||||||
|
vec2 sampleTexSize = u_TexelSize / actualSampleSize;
|
||||||
|
// sample is taken from center of fragment
|
||||||
|
// this moves the coordinates to the starting corner and to the center of the sample texel
|
||||||
|
vec2 sampleOrigin = v_TexCoord - sampleTexSize * (actualSampleSize / 2.0 - 0.5);
|
||||||
|
float sampleCount = 0.0;
|
||||||
|
for(float x = 0.0; x < MAX_SAMPLE_SIZE; x++) {
|
||||||
|
if(x >= u_SampleSize.x) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for(float y = 0.0; y < MAX_SAMPLE_SIZE; y++) {
|
||||||
|
if(y >= u_SampleSize.y) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
vec2 pos = sampleOrigin + sampleTexSize * vec2(x, y);
|
||||||
|
vec4 sample = texture2D(u_Template, pos);
|
||||||
|
// pxlsfiddle uses the alpha channel of the first pixel to store
|
||||||
|
// scale information. This can affect color sampling, so drop the
|
||||||
|
// top-left-most subtexel unless its alpha is typical (1 or 0 exactly).
|
||||||
|
if(x == 0.0 && y == 0.0
|
||||||
|
&& pos.x < u_TexelSize.x && (1.0 - pos.y) < u_TexelSize.y
|
||||||
|
&& sample.a != 1.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(sample.a == 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
color += sample;
|
||||||
|
sampleCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(sampleCount == 0.0) {
|
||||||
|
gl_FragColor = transparentColor;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
color /= sampleCount;
|
||||||
|
#ifdef CONVERT_COLORS
|
||||||
|
float bestDiff = HIGHEST_DIFF;
|
||||||
|
int bestIndex = int(PALETTE_MAXSIZE);
|
||||||
|
vec3 bestColor = vec3(0.0);
|
||||||
|
for(int i = 0; i < PALETTE_LENGTH; i++) {
|
||||||
|
float diff = ${comparisonFunctionName}(color.rgb, u_Palette[i]);
|
||||||
|
if(diff < bestDiff) {
|
||||||
|
bestDiff = diff;
|
||||||
|
bestIndex = i;
|
||||||
|
bestColor = u_Palette[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gl_FragColor = vec4(bestColor, float(bestIndex) / PALETTE_MAXSIZE);
|
||||||
|
#else
|
||||||
|
for(int i = 0; i < PALETTE_LENGTH; i++) {
|
||||||
|
if(all(lessThan(abs(u_Palette[i] - color.rgb), vec3(epsilon)))) {
|
||||||
|
gl_FragColor = vec4(u_Palette[i], float(i) / PALETTE_MAXSIZE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gl_FragColor = vec4(color.rgb, PALETTE_UNKNOWN);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
this.programs.downscaling.unconverted = utils.createProgram(
|
||||||
|
identityVertexShader,
|
||||||
|
downscalingFragmentShader()
|
||||||
|
);
|
||||||
|
this.programs.downscaling.nearestCustom = utils.createProgram(
|
||||||
|
identityVertexShader,
|
||||||
|
downscalingFragmentShader("diffCustom")
|
||||||
|
);
|
||||||
|
const int2rgb = (i: number) => [
|
||||||
|
(i >> 16) & 0xff,
|
||||||
|
(i >> 8) & 0xff,
|
||||||
|
i & 0xff,
|
||||||
|
];
|
||||||
|
const paletteBuffer = new Float32Array(
|
||||||
|
palette.flatMap((c) => int2rgb(parseInt(c.value, 16)).map((c) => c / 255))
|
||||||
|
);
|
||||||
|
for (const program of Object.values(this.programs.downscaling)) {
|
||||||
|
context.useProgram(program);
|
||||||
|
const posLocation = context.getAttribLocation(program, "a_Pos");
|
||||||
|
context.vertexAttribPointer(posLocation, 2, context.FLOAT, false, 0, 0);
|
||||||
|
context.enableVertexAttribArray(posLocation);
|
||||||
|
context.uniform1i(context.getUniformLocation(program, "u_Template"), 0);
|
||||||
|
context.uniform3fv(
|
||||||
|
context.getUniformLocation(program, "u_Palette"),
|
||||||
|
paletteBuffer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.programs.stylize = utils.createProgram(
|
||||||
|
identityVertexShader,
|
||||||
|
`
|
||||||
|
precision mediump float;
|
||||||
|
#define STYLES_X float(${STYLES_X})
|
||||||
|
#define STYLES_Y float(${STYLES_Y})
|
||||||
|
${paletteDefs}
|
||||||
|
uniform sampler2D u_Template;
|
||||||
|
uniform sampler2D u_Style;
|
||||||
|
uniform vec2 u_TexelSize;
|
||||||
|
varying vec2 v_TexCoord;
|
||||||
|
const vec2 styleSize = vec2(1.0 / STYLES_X, 1.0 / STYLES_Y);
|
||||||
|
void main () {
|
||||||
|
vec4 templateSample = texture2D(u_Template, v_TexCoord);
|
||||||
|
float index = floor(templateSample.a * PALETTE_MAXSIZE + 0.5);
|
||||||
|
vec2 indexCoord = vec2(mod(index, STYLES_X), STYLES_Y - floor(index / STYLES_Y) - 1.0);
|
||||||
|
vec2 subTexCoord = mod(v_TexCoord, u_TexelSize) / u_TexelSize;
|
||||||
|
vec2 styleCoord = (indexCoord + subTexCoord) * styleSize;
|
||||||
|
|
||||||
|
vec4 styleMask = vec4(1.0, 1.0, 1.0, texture2D(u_Style, styleCoord).a);
|
||||||
|
gl_FragColor = vec4(templateSample.rgb, templateSample.a == PALETTE_TRANSPARENT ? 0.0 : 1.0) * styleMask;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
context.useProgram(this.programs.stylize);
|
||||||
|
const stylePosLocation = context.getAttribLocation(
|
||||||
|
this.programs.stylize,
|
||||||
|
"a_Pos"
|
||||||
|
);
|
||||||
|
context.vertexAttribPointer(
|
||||||
|
stylePosLocation,
|
||||||
|
2,
|
||||||
|
context.FLOAT,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
context.enableVertexAttribArray(stylePosLocation);
|
||||||
|
context.uniform1i(
|
||||||
|
context.getUniformLocation(this.programs.stylize, "u_Template"),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
context.uniform1i(
|
||||||
|
context.getUniformLocation(this.programs.stylize, "u_Style"),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStyle(redraw = true) {
|
||||||
|
if (
|
||||||
|
this.context &&
|
||||||
|
this.$style.naturalWidth !== 0 &&
|
||||||
|
this.$style.naturalHeight !== 0
|
||||||
|
) {
|
||||||
|
this.context.activeTexture(this.context.TEXTURE1);
|
||||||
|
this.context.bindTexture(this.context.TEXTURE_2D, this.textures.style);
|
||||||
|
this.context.texImage2D(
|
||||||
|
this.context.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
this.context.ALPHA,
|
||||||
|
this.context.ALPHA,
|
||||||
|
this.context.UNSIGNED_BYTE,
|
||||||
|
this.$style
|
||||||
|
);
|
||||||
|
|
||||||
|
if (redraw) {
|
||||||
|
this.stylizeTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downscaleTemplate() {
|
||||||
|
const dimentions = this.getDimentions();
|
||||||
|
const {
|
||||||
|
display: { width, height },
|
||||||
|
} = dimentions;
|
||||||
|
if (!this.context || width === 0 || height === 0) return;
|
||||||
|
|
||||||
|
const downscaleWidth = dimentions.source.width / dimentions.display.width;
|
||||||
|
const downscaleHeight =
|
||||||
|
dimentions.source.height / dimentions.display.height;
|
||||||
|
|
||||||
|
// set size of framebuffer
|
||||||
|
this.context.activeTexture(this.context.TEXTURE0);
|
||||||
|
this.context.bindTexture(this.context.TEXTURE_2D, this.textures.downscaled);
|
||||||
|
this.context.texImage2D(
|
||||||
|
this.context.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
this.context.RGBA,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
0,
|
||||||
|
this.context.RGBA,
|
||||||
|
this.context.UNSIGNED_BYTE,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
this.context.bindFramebuffer(
|
||||||
|
this.context.FRAMEBUFFER,
|
||||||
|
this.framebuffers.intermediate
|
||||||
|
);
|
||||||
|
this.context.clear(this.context.COLOR_BUFFER_BIT);
|
||||||
|
this.context.viewport(0, 0, width, height);
|
||||||
|
|
||||||
|
const program = this.programs.downscaling.nearestCustom;
|
||||||
|
|
||||||
|
this.context.useProgram(program);
|
||||||
|
|
||||||
|
this.context.uniform2f(
|
||||||
|
this.context.getUniformLocation(program, "u_SampleSize"),
|
||||||
|
Math.max(1, downscaleWidth),
|
||||||
|
Math.max(1, downscaleHeight)
|
||||||
|
);
|
||||||
|
this.context.uniform2f(
|
||||||
|
this.context.getUniformLocation(program, "u_TexelSize"),
|
||||||
|
1 / width,
|
||||||
|
1 / height
|
||||||
|
);
|
||||||
|
|
||||||
|
this.context.bindTexture(this.context.TEXTURE_2D, this.textures.source);
|
||||||
|
|
||||||
|
this.context.texImage2D(
|
||||||
|
this.context.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
this.context.RGBA,
|
||||||
|
this.context.RGBA,
|
||||||
|
this.context.UNSIGNED_BYTE,
|
||||||
|
this.$imageLoader
|
||||||
|
);
|
||||||
|
|
||||||
|
this.context.drawArrays(this.context.TRIANGLE_STRIP, 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
stylizeTemplate() {
|
||||||
|
this.updateSize();
|
||||||
|
|
||||||
|
const {
|
||||||
|
display: { width, height },
|
||||||
|
} = this.getDimentions();
|
||||||
|
|
||||||
|
if (this.context == null || width === 0 || height === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.bindFramebuffer(
|
||||||
|
this.context.FRAMEBUFFER,
|
||||||
|
this.framebuffers.main
|
||||||
|
);
|
||||||
|
this.context.clear(this.context.COLOR_BUFFER_BIT);
|
||||||
|
this.context.viewport(0, 0, width, height);
|
||||||
|
|
||||||
|
this.context.useProgram(this.programs.stylize);
|
||||||
|
|
||||||
|
this.context.uniform2f(
|
||||||
|
this.context.getUniformLocation(this.programs.stylize, "u_TexelSize"),
|
||||||
|
1 / width,
|
||||||
|
1 / height
|
||||||
|
);
|
||||||
|
|
||||||
|
this.context.activeTexture(this.context.TEXTURE0);
|
||||||
|
this.context.bindTexture(this.context.TEXTURE_2D, this.textures.downscaled);
|
||||||
|
|
||||||
|
this.context.activeTexture(this.context.TEXTURE1);
|
||||||
|
this.context.bindTexture(this.context.TEXTURE_2D, this.textures.style);
|
||||||
|
|
||||||
|
this.context.drawArrays(this.context.TRIANGLE_STRIP, 0, 4);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* Utilities for WebGL contexts
|
||||||
|
*/
|
||||||
|
export class WebGLUtils {
|
||||||
|
context: WebGLRenderingContext;
|
||||||
|
|
||||||
|
constructor(context: WebGLRenderingContext) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create WebGL texture
|
||||||
|
*
|
||||||
|
* Originates from Pxls
|
||||||
|
*
|
||||||
|
* @returns WebGL texture
|
||||||
|
*/
|
||||||
|
createTexture() {
|
||||||
|
const texture = this.context.createTexture();
|
||||||
|
this.context.bindTexture(this.context.TEXTURE_2D, texture);
|
||||||
|
this.context.texParameteri(
|
||||||
|
this.context.TEXTURE_2D,
|
||||||
|
this.context.TEXTURE_WRAP_S,
|
||||||
|
this.context.CLAMP_TO_EDGE
|
||||||
|
);
|
||||||
|
this.context.texParameteri(
|
||||||
|
this.context.TEXTURE_2D,
|
||||||
|
this.context.TEXTURE_WRAP_T,
|
||||||
|
this.context.CLAMP_TO_EDGE
|
||||||
|
);
|
||||||
|
this.context.texParameteri(
|
||||||
|
this.context.TEXTURE_2D,
|
||||||
|
this.context.TEXTURE_MIN_FILTER,
|
||||||
|
this.context.NEAREST
|
||||||
|
);
|
||||||
|
this.context.texParameteri(
|
||||||
|
this.context.TEXTURE_2D,
|
||||||
|
this.context.TEXTURE_MAG_FILTER,
|
||||||
|
this.context.NEAREST
|
||||||
|
);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create WebGL program
|
||||||
|
*
|
||||||
|
* Originates from Pxls
|
||||||
|
*
|
||||||
|
* @param vertexSource
|
||||||
|
* @param fragmentSource
|
||||||
|
* @returns WebGL program
|
||||||
|
*/
|
||||||
|
createProgram(vertexSource: string, fragmentSource: string) {
|
||||||
|
// i believe null is only returned when webgl context is destroyed?
|
||||||
|
// maybe add proper handling here
|
||||||
|
const program = this.context.createProgram()!;
|
||||||
|
this.context.attachShader(
|
||||||
|
program,
|
||||||
|
this.createShader(this.context.VERTEX_SHADER, vertexSource)
|
||||||
|
);
|
||||||
|
this.context.attachShader(
|
||||||
|
program,
|
||||||
|
this.createShader(this.context.FRAGMENT_SHADER, fragmentSource)
|
||||||
|
);
|
||||||
|
this.context.linkProgram(program);
|
||||||
|
if (!this.context.getProgramParameter(program, this.context.LINK_STATUS)) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to link WebGL template program:\n\n${this.context.getProgramInfoLog(program)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create WebGL shader
|
||||||
|
*
|
||||||
|
* Originates from Pxls
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
* @param source
|
||||||
|
* @returns WebGL shader
|
||||||
|
*/
|
||||||
|
createShader(type: number, source: string) {
|
||||||
|
// i believe null is only returned when webgl context is destroyed?
|
||||||
|
// maybe add proper handling here
|
||||||
|
const shader = this.context.createShader(type)!;
|
||||||
|
|
||||||
|
this.context.shaderSource(shader, source);
|
||||||
|
this.context.compileShader(shader);
|
||||||
|
|
||||||
|
if (!this.context.getShaderParameter(shader, this.context.COMPILE_STATUS)) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to compile WebGL template shader:\n\n${this.context.getShaderInfoLog(shader)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ header#main-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 9999;
|
z-index: 9980;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
@ -142,5 +142,58 @@ main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 9998;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
min-width: 20rem;
|
||||||
|
max-width: 75vw;
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 5px 1rem;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
input {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@import "./components/Pallete.scss";
|
@import "./components/Pallete.scss";
|
||||||
|
@import "./components/Template.scss";
|
||||||
@import "./board.scss";
|
@import "./board.scss";
|
||||||
|
|
|
@ -32,6 +32,8 @@ export interface IAppContext {
|
||||||
cursorPosition?: IPosition;
|
cursorPosition?: IPosition;
|
||||||
setCursorPosition: (v?: IPosition) => void;
|
setCursorPosition: (v?: IPosition) => void;
|
||||||
pixels: { available: number };
|
pixels: { available: number };
|
||||||
|
settingsSidebar: boolean;
|
||||||
|
setSettingsSidebar: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPalleteContext {
|
export interface IPalleteContext {
|
||||||
|
|
Loading…
Reference in New Issue