mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Cowboy prototyping
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user