Cowboy prototyping

This commit is contained in:
Danny McGee
2023-10-10 19:46:40 -04:00
parent c84fd31387
commit 86a948ef09
17 changed files with 1191 additions and 40 deletions
+85
View File
@@ -10,6 +10,91 @@
right: 16px;
}
.toolbar {
display: flex;
gap: 16px;
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
mix-blend-mode: multiply;
}
.toolbar-group {
display: flex;
gap: 2px;
mix-blend-mode: multiply;
}
.btn-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 15px;
padding: 10px 12px;
pointer-events: auto;
// TODO: Copypasta
background: #0006;
color: #FFFC;
backdrop-filter: blur(16px);
transition: {
property: background, color;
duration: 0.1s;
timing-function: linear;
}
mix-blend-mode: multiply;
&:first-child {
border: {
top-left-radius: 8px;
bottom-left-radius: 8px;
}
}
&:last-child {
border: {
top-right-radius: 8px;
bottom-right-radius: 8px;
}
}
&:hover {
background: #0008;
color: #FFF;
}
&:has(:checked) {
background: #7A2C34E3;
color: #FFF;
background-blend-mode: multiply;
&:hover {
background: #90363F;
}
}
&:has(:disabled) {
background: #0003 !important;
color: #FFF6 !important;
}
input {
display: block;
appearance: none;
border: 0;
margin: 0;
padding: 0;
opacity: 0;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
}
}
.help {
position: absolute;
top: 60px;
+115 -2
View File
@@ -1,14 +1,21 @@
import { on } from "@storyteller/framework";
import * as studio from "@storyteller/studio";
import { ViewportSize } from "@storyteller/studio";
import { TransformSpace, ViewportSize } from "@storyteller/studio";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./button.element";
import "./help.element";
import "./icons/fa-circle-info.element";
import "./icon.element";
import styles from "./app.element.scss?inline";
import { match } from "@storyteller/utility";
// FIXME: Figure out how to generate the correct code from `wasm_bindgen`
enum InteractionTransform {
TRANSLATE = 0b0001,
ROTATE = 0b0010,
}
@customElement("app-root")
export class AppElement extends LitElement {
@@ -16,6 +23,12 @@ export class AppElement extends LitElement {
@state() showHelp = false;
@state() transformSpace: "local"|"world" = "local";
@state() translateGizmoEnabled = true;
@state() rotateGizmoEnabled = true;
@state() scaleGizmoEnabled = false;
#studioCanvas?: HTMLCanvasElement | null;
override connectedCallback(): void {
@@ -34,6 +47,58 @@ export class AppElement extends LitElement {
this.showHelp = !this.showHelp;
}
onSpaceChange(event: InputEvent): void {
const { value } = event.target as HTMLInputElement;
if (value === this.transformSpace)
return;
match (value, {
"local": () => studio.setTransformSpace(TransformSpace.Local),
"world": () => studio.setTransformSpace(TransformSpace.World),
_: () => {
console.warn(`Unknown transform space: "${value}"`);
return;
}
});
this.transformSpace = value as "local"|"world";
}
onGizmoToggled(event: InputEvent): void {
const { name, checked } = event.target as HTMLInputElement;
match (name, {
"translate": () => {
if (this.translateGizmoEnabled === checked)
return;
this.translateGizmoEnabled = checked;
},
"rotate": () => {
if (this.rotateGizmoEnabled === checked)
return;
this.rotateGizmoEnabled = checked;
},
"scale": () => {
if (this.scaleGizmoEnabled === checked)
return;
this.scaleGizmoEnabled = checked;
},
_: () => {
console.warn(`Unknown gizmo name: "${name}"`);
return;
}
});
let transform = 0;
if (this.translateGizmoEnabled) transform |= InteractionTransform.TRANSLATE;
if (this.rotateGizmoEnabled) transform |= InteractionTransform.ROTATE;
// TODO: Scale
studio.setInteractionTransform(transform);
}
#initStudio(): HTMLCanvasElement | null {
const width = window.innerWidth;
const height = window.innerHeight;
@@ -48,6 +113,54 @@ export class AppElement extends LitElement {
}
protected override render = () => html`
<!-- TODO: What's a "component"? -->
<div class="toolbar" role="toolbar">
<div class="toolbar-group" role="group">
<label class="btn-toggle">
<sts-icon icon=${"cube"}>Local</sts-icon>
<input type="radio"
name="transform-space"
value="local"
checked
@input=${this.onSpaceChange}
/>
</label>
<label class="btn-toggle">
<sts-icon icon="globe">Global</sts-icon>
<input type="radio"
name="transform-space"
value="world"
@input=${this.onSpaceChange}
/>
</label>
</div>
<div class="toolbar-group" role="group">
<label class="btn-toggle">
<sts-icon icon="up-down-left-right">Translate</sts-icon>
<input type="checkbox"
name="translate"
checked
@input=${this.onGizmoToggled}
/>
</label>
<label class="btn-toggle">
<sts-icon icon="rotate">Rotate</sts-icon>
<input type="checkbox"
name="rotate"
checked
@input=${this.onGizmoToggled}
/>
</label>
<label class="btn-toggle">
<sts-icon icon="scale">Scale</sts-icon>
<input type="checkbox"
name="scale"
disabled
/>
</label>
</div>
</div>
<sts-button class="btn-help"
icon="circle-info"
@click=${this.toggleHelp}
+5 -6
View File
@@ -1,6 +1,5 @@
import { match } from "@storyteller/utility";
import { on } from "@storyteller/framework";
import { LitElement, html, unsafeCSS } from "lit";
import { LitElement, html, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import styles from "./button.element.scss?inline";
@@ -21,10 +20,10 @@ export class ButtonElement extends LitElement {
}
protected override render = () => html`
${match (this.icon, {
"circle-info": () => html`<fa-circle-info class="icon"></fa-circle-info>`,
_: () => null,
})}
${this.icon
? html`<sts-icon icon=${this.icon} aria-hidden="true"></sts-icon>`
: nothing}
<slot></slot>
`
}
+28
View File
@@ -0,0 +1,28 @@
:host {
display: flex;
width: auto;
align-items: center;
justify-content: center;
height: 1em;
}
.a11y-label {
position: absolute;
overflow: hidden;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
clip: rect(1px, 1px, 1px, 1px);
border: 0;
white-space: nowrap;
}
.svg-wrapper {
display: flex;
}
svg {
height: 1em;
fill: currentColor;
}
+69
View File
@@ -0,0 +1,69 @@
import { match } from "@storyteller/utility";
import { ElementPart, LitElement, html, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Directive, directive } from "lit/directive.js";
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
import fasCircleInfo from "@fortawesome/fontawesome-pro/svgs/solid/circle-info.svg?raw";
import fasCube from "@fortawesome/fontawesome-pro/svgs/solid/cube.svg?raw";
import fasGlobe from "@fortawesome/fontawesome-pro/svgs/solid/globe.svg?raw";
import fasUpDownLeftRight from "@fortawesome/fontawesome-pro/svgs/solid/up-down-left-right.svg?raw";
import fasRotate from "@fortawesome/fontawesome-pro/svgs/solid/rotate.svg?raw";
import fasScale from "@fortawesome/fontawesome-pro/svgs/solid/arrow-up-right-and-arrow-down-left-from-center.svg?raw";
import styles from "./icon.element.scss?inline";
@customElement("sts-icon")
export class IconElement extends LitElement {
static override styles = unsafeCSS(styles);
@property() icon = "";
protected override render = () => html`
<span class="a11y-label">
<slot></slot>
</span>
<div class="svg-wrapper" role="presentation">
${match (this.icon, {
"circle-info": () => renderSvg(fasCircleInfo),
"cube": () => renderSvg(fasCube),
"globe": () => renderSvg(fasGlobe),
"up-down-left-right": () => renderSvg(fasUpDownLeftRight),
"rotate": () => renderSvg(fasRotate),
"scale": () => renderSvg(fasScale),
_: () => nothing,
})}
</div>
`;
}
// TODO: Consider moving this
function renderSvg(fullSvg: string) {
console.log("parsing:", fullSvg);
const [, attrsString, body] = fullSvg.match(/<svg(.+?)>(.+?)<\/svg>/)!;
const attrs = Object.fromEntries(
Array.from(attrsString.matchAll(/([-:_a-zA-Z0-9]+)(?:=['"]?([^'"]*)['"]?)/g))
.map(([, attr, value]) => [attr, value])
);
return html`
<svg ${spread(attrs)}>
${unsafeSVG(body)}
</svg>
`;
}
// TODO: Definitely move this
const spread = directive(class extends Directive {
override render(_: Record<string, string>) {
return nothing;
}
override update(part: ElementPart, [attrs]: [Record<string, string>]) {
for (let [key, value] of Object.entries(attrs)) {
part.element.setAttribute(key, value);
}
return part;
}
});