@livepeer-frameworks/streamcrafter-wc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/fw-sc-advanced.js +198 -0
- package/dist/cjs/components/fw-sc-advanced.js.map +1 -0
- package/dist/cjs/components/fw-sc-compositor.js +116 -0
- package/dist/cjs/components/fw-sc-compositor.js.map +1 -0
- package/dist/cjs/components/fw-sc-layer-list.js +253 -0
- package/dist/cjs/components/fw-sc-layer-list.js.map +1 -0
- package/dist/cjs/components/fw-sc-scene-switcher.js +164 -0
- package/dist/cjs/components/fw-sc-scene-switcher.js.map +1 -0
- package/dist/cjs/components/fw-sc-volume.js +183 -0
- package/dist/cjs/components/fw-sc-volume.js.map +1 -0
- package/dist/cjs/components/fw-streamcrafter.js +508 -0
- package/dist/cjs/components/fw-streamcrafter.js.map +1 -0
- package/dist/cjs/controllers/ingest-controller-host.js +236 -0
- package/dist/cjs/controllers/ingest-controller-host.js.map +1 -0
- package/dist/cjs/define.js +25 -0
- package/dist/cjs/define.js.map +1 -0
- package/dist/cjs/icons/index.js +283 -0
- package/dist/cjs/icons/index.js.map +1 -0
- package/dist/cjs/index.js +38 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/node_modules/.pnpm/@rollup_plugin-typescript@12.3.0_rollup@4.57.1_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js +33 -0
- package/dist/cjs/node_modules/.pnpm/@rollup_plugin-typescript@12.3.0_rollup@4.57.1_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js.map +1 -0
- package/dist/cjs/styles/shared-styles.js +2019 -0
- package/dist/cjs/styles/shared-styles.js.map +1 -0
- package/dist/cjs/styles/utility-styles.js +182 -0
- package/dist/cjs/styles/utility-styles.js.map +1 -0
- package/dist/esm/components/fw-sc-advanced.js +198 -0
- package/dist/esm/components/fw-sc-advanced.js.map +1 -0
- package/dist/esm/components/fw-sc-compositor.js +116 -0
- package/dist/esm/components/fw-sc-compositor.js.map +1 -0
- package/dist/esm/components/fw-sc-layer-list.js +253 -0
- package/dist/esm/components/fw-sc-layer-list.js.map +1 -0
- package/dist/esm/components/fw-sc-scene-switcher.js +164 -0
- package/dist/esm/components/fw-sc-scene-switcher.js.map +1 -0
- package/dist/esm/components/fw-sc-volume.js +183 -0
- package/dist/esm/components/fw-sc-volume.js.map +1 -0
- package/dist/esm/components/fw-streamcrafter.js +508 -0
- package/dist/esm/components/fw-streamcrafter.js.map +1 -0
- package/dist/esm/controllers/ingest-controller-host.js +234 -0
- package/dist/esm/controllers/ingest-controller-host.js.map +1 -0
- package/dist/esm/define.js +23 -0
- package/dist/esm/define.js.map +1 -0
- package/dist/esm/icons/index.js +253 -0
- package/dist/esm/icons/index.js.map +1 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/@rollup_plugin-typescript@12.3.0_rollup@4.57.1_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js +31 -0
- package/dist/esm/node_modules/.pnpm/@rollup_plugin-typescript@12.3.0_rollup@4.57.1_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js.map +1 -0
- package/dist/esm/styles/shared-styles.js +2017 -0
- package/dist/esm/styles/shared-styles.js.map +1 -0
- package/dist/esm/styles/utility-styles.js +180 -0
- package/dist/esm/styles/utility-styles.js.map +1 -0
- package/dist/fw-streamcrafter.iife.js +3121 -0
- package/dist/fw-streamcrafter.iife.js.map +1 -0
- package/dist/types/components/fw-sc-advanced.d.ts +20 -0
- package/dist/types/components/fw-sc-compositor.d.ts +19 -0
- package/dist/types/components/fw-sc-layer-list.d.ts +30 -0
- package/dist/types/components/fw-sc-scene-switcher.d.ts +23 -0
- package/dist/types/components/fw-sc-volume.d.ts +30 -0
- package/dist/types/components/fw-streamcrafter.d.ts +49 -0
- package/dist/types/controllers/ingest-controller-host.d.ts +77 -0
- package/dist/types/define.d.ts +1 -0
- package/dist/types/icons/index.d.ts +29 -0
- package/dist/types/iife-entry.d.ts +11 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/styles/shared-styles.d.ts +1 -0
- package/dist/types/styles/utility-styles.d.ts +1 -0
- package/package.json +55 -0
- package/src/components/fw-sc-advanced.ts +221 -0
- package/src/components/fw-sc-compositor.ts +162 -0
- package/src/components/fw-sc-layer-list.ts +251 -0
- package/src/components/fw-sc-scene-switcher.ts +163 -0
- package/src/components/fw-sc-volume.ts +171 -0
- package/src/components/fw-streamcrafter.ts +515 -0
- package/src/controllers/ingest-controller-host.ts +358 -0
- package/src/define.ts +23 -0
- package/src/icons/index.ts +291 -0
- package/src/iife-entry.ts +11 -0
- package/src/index.ts +15 -0
- package/src/styles/shared-styles.ts +2014 -0
- package/src/styles/utility-styles.ts +177 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <fw-sc-compositor> — Compact floating compositor controls overlay.
|
|
3
|
+
* Port of CompositorControls.tsx from streamcrafter-react.
|
|
4
|
+
*/
|
|
5
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
6
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
7
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
8
|
+
import { sharedStyles } from "../styles/shared-styles.js";
|
|
9
|
+
import { utilityStyles } from "../styles/utility-styles.js";
|
|
10
|
+
import {
|
|
11
|
+
soloIcon,
|
|
12
|
+
pipBRIcon,
|
|
13
|
+
pipBLIcon,
|
|
14
|
+
pipTRIcon,
|
|
15
|
+
pipTLIcon,
|
|
16
|
+
splitHIcon,
|
|
17
|
+
splitVIcon,
|
|
18
|
+
focusLIcon,
|
|
19
|
+
focusRIcon,
|
|
20
|
+
gridIcon,
|
|
21
|
+
stackIcon,
|
|
22
|
+
dualPipIcon,
|
|
23
|
+
splitPipIcon,
|
|
24
|
+
featuredIcon,
|
|
25
|
+
featuredRIcon,
|
|
26
|
+
letterboxIcon,
|
|
27
|
+
cropIcon,
|
|
28
|
+
stretchIcon,
|
|
29
|
+
} from "../icons/index.js";
|
|
30
|
+
import type { IngestControllerHost } from "../controllers/ingest-controller-host.js";
|
|
31
|
+
import type {
|
|
32
|
+
LayoutMode,
|
|
33
|
+
LayoutConfig,
|
|
34
|
+
ScalingMode,
|
|
35
|
+
} from "@livepeer-frameworks/streamcrafter-core";
|
|
36
|
+
import { isLayoutAvailable } from "@livepeer-frameworks/streamcrafter-core";
|
|
37
|
+
|
|
38
|
+
interface LayoutPresetUI {
|
|
39
|
+
mode: LayoutMode;
|
|
40
|
+
label: string;
|
|
41
|
+
icon: () => ReturnType<typeof soloIcon>;
|
|
42
|
+
minSources: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const LAYOUT_PRESETS_UI: LayoutPresetUI[] = [
|
|
46
|
+
{ mode: "solo", label: "Solo", icon: soloIcon, minSources: 1 },
|
|
47
|
+
{ mode: "pip-br", label: "PiP ↘", icon: pipBRIcon, minSources: 2 },
|
|
48
|
+
{ mode: "pip-bl", label: "PiP ↙", icon: pipBLIcon, minSources: 2 },
|
|
49
|
+
{ mode: "pip-tr", label: "PiP ↗", icon: pipTRIcon, minSources: 2 },
|
|
50
|
+
{ mode: "pip-tl", label: "PiP ↖", icon: pipTLIcon, minSources: 2 },
|
|
51
|
+
{ mode: "split-h", label: "Split ⬌", icon: splitHIcon, minSources: 2 },
|
|
52
|
+
{ mode: "split-v", label: "Split ⬍", icon: splitVIcon, minSources: 2 },
|
|
53
|
+
{ mode: "focus-l", label: "Focus ◀", icon: focusLIcon, minSources: 2 },
|
|
54
|
+
{ mode: "focus-r", label: "Focus ▶", icon: focusRIcon, minSources: 2 },
|
|
55
|
+
{ mode: "pip-dual-br", label: "Main+2 PiP", icon: dualPipIcon, minSources: 3 },
|
|
56
|
+
{ mode: "split-pip-r", label: "Split+PiP", icon: splitPipIcon, minSources: 3 },
|
|
57
|
+
{ mode: "featured", label: "Featured", icon: featuredIcon, minSources: 3 },
|
|
58
|
+
{ mode: "featured-r", label: "Featured ▶", icon: featuredRIcon, minSources: 3 },
|
|
59
|
+
{ mode: "grid", label: "Grid", icon: gridIcon, minSources: 2 },
|
|
60
|
+
{ mode: "stack", label: "Stack", icon: stackIcon, minSources: 2 },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const SCALING_MODES: {
|
|
64
|
+
mode: ScalingMode;
|
|
65
|
+
icon: () => ReturnType<typeof letterboxIcon>;
|
|
66
|
+
label: string;
|
|
67
|
+
}[] = [
|
|
68
|
+
{ mode: "letterbox", icon: letterboxIcon, label: "Letterbox (fit)" },
|
|
69
|
+
{ mode: "crop", icon: cropIcon, label: "Crop (fill)" },
|
|
70
|
+
{ mode: "stretch", icon: stretchIcon, label: "Stretch" },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
@customElement("fw-sc-compositor")
|
|
74
|
+
export class FwScCompositor extends LitElement {
|
|
75
|
+
@property({ attribute: false }) ic!: IngestControllerHost;
|
|
76
|
+
|
|
77
|
+
@state() private _tooltipText = "";
|
|
78
|
+
@state() private _tooltipTarget: Element | null = null;
|
|
79
|
+
|
|
80
|
+
static styles = [
|
|
81
|
+
sharedStyles,
|
|
82
|
+
utilityStyles,
|
|
83
|
+
css`
|
|
84
|
+
:host {
|
|
85
|
+
display: contents;
|
|
86
|
+
}
|
|
87
|
+
`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
protected render() {
|
|
91
|
+
// Compositor uses controller compositor API — for now render layout bar from CSS classes
|
|
92
|
+
const sources = this.ic.s.sources;
|
|
93
|
+
const visibleSourceCount = sources.length;
|
|
94
|
+
|
|
95
|
+
const availableLayouts = LAYOUT_PRESETS_UI.filter((preset) =>
|
|
96
|
+
isLayoutAvailable(preset.mode, visibleSourceCount)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return html`
|
|
100
|
+
<div class="fw-sc-layout-overlay">
|
|
101
|
+
<div class="fw-sc-layout-bar">
|
|
102
|
+
<div class="fw-sc-layout-section">
|
|
103
|
+
<span class="fw-sc-layout-label">Layout</span>
|
|
104
|
+
<div class="fw-sc-layout-icons">
|
|
105
|
+
${availableLayouts.map(
|
|
106
|
+
(preset) => html`
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
class="fw-sc-layout-icon"
|
|
110
|
+
@click=${(e: MouseEvent) => {
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
this._handleLayoutSelect(preset.mode);
|
|
113
|
+
}}
|
|
114
|
+
title=${preset.label}
|
|
115
|
+
>
|
|
116
|
+
${preset.icon()}
|
|
117
|
+
</button>
|
|
118
|
+
`
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="fw-sc-layout-separator"></div>
|
|
123
|
+
<div class="fw-sc-layout-section">
|
|
124
|
+
<span class="fw-sc-layout-label">Display</span>
|
|
125
|
+
<div class="fw-sc-scaling-icons">
|
|
126
|
+
${SCALING_MODES.map(
|
|
127
|
+
(sm) => html`
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
class="fw-sc-layout-icon"
|
|
131
|
+
@click=${(e: MouseEvent) => {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
}}
|
|
134
|
+
title=${sm.label}
|
|
135
|
+
>
|
|
136
|
+
${sm.icon()}
|
|
137
|
+
</button>
|
|
138
|
+
`
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private _handleLayoutSelect(mode: LayoutMode) {
|
|
148
|
+
this.dispatchEvent(
|
|
149
|
+
new CustomEvent("fw-sc-layout-select", {
|
|
150
|
+
detail: { mode },
|
|
151
|
+
bubbles: true,
|
|
152
|
+
composed: true,
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
declare global {
|
|
159
|
+
interface HTMLElementTagNameMap {
|
|
160
|
+
"fw-sc-compositor": FwScCompositor;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <fw-sc-layer-list> — Drag-to-reorder layer list.
|
|
3
|
+
* Port of LayerList.tsx from streamcrafter-react.
|
|
4
|
+
*/
|
|
5
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
6
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
7
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
8
|
+
import { sharedStyles } from "../styles/shared-styles.js";
|
|
9
|
+
import { utilityStyles } from "../styles/utility-styles.js";
|
|
10
|
+
import { eyeIcon, eyeOffIcon, cameraIcon, monitorIcon, videoIcon } from "../icons/index.js";
|
|
11
|
+
import type { Layer, MediaSource, LayerTransform } from "@livepeer-frameworks/streamcrafter-core";
|
|
12
|
+
|
|
13
|
+
@customElement("fw-sc-layer-list")
|
|
14
|
+
export class FwScLayerList extends LitElement {
|
|
15
|
+
@property({ attribute: false }) layers: Layer[] = [];
|
|
16
|
+
@property({ attribute: false }) sources: MediaSource[] = [];
|
|
17
|
+
@property({ type: String, attribute: "selected-layer-id" }) selectedLayerId: string | null = null;
|
|
18
|
+
|
|
19
|
+
@state() private _draggedId: string | null = null;
|
|
20
|
+
@state() private _dragOverId: string | null = null;
|
|
21
|
+
@state() private _editingLayerId: string | null = null;
|
|
22
|
+
|
|
23
|
+
static styles = [
|
|
24
|
+
sharedStyles,
|
|
25
|
+
utilityStyles,
|
|
26
|
+
css`
|
|
27
|
+
:host {
|
|
28
|
+
display: block;
|
|
29
|
+
}
|
|
30
|
+
`,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
private get _sortedLayers(): Layer[] {
|
|
34
|
+
return [...this.layers].sort((a, b) => b.zIndex - a.zIndex);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private _getSourceLabel(sourceId: string): string {
|
|
38
|
+
const source = this.sources.find((s) => s.id === sourceId);
|
|
39
|
+
return source?.label || sourceId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private _getSourceIcon(sourceId: string) {
|
|
43
|
+
const source = this.sources.find((s) => s.id === sourceId);
|
|
44
|
+
switch (source?.type) {
|
|
45
|
+
case "camera":
|
|
46
|
+
return cameraIcon(14);
|
|
47
|
+
case "screen":
|
|
48
|
+
return monitorIcon(14);
|
|
49
|
+
default:
|
|
50
|
+
return videoIcon(14);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected render() {
|
|
55
|
+
const sorted = this._sortedLayers;
|
|
56
|
+
return html`
|
|
57
|
+
<div class="fw-sc-layer-list">
|
|
58
|
+
<div class="fw-sc-layer-list-header">
|
|
59
|
+
<span class="fw-sc-layer-list-title">Layers</span>
|
|
60
|
+
<span class="fw-sc-layer-count">${this.layers.length}</span>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="fw-sc-layer-items">
|
|
64
|
+
${sorted.length === 0
|
|
65
|
+
? html` <div class="fw-sc-layer-empty">No layers. Add a source to get started.</div> `
|
|
66
|
+
: sorted.map(
|
|
67
|
+
(layer, index) => html`
|
|
68
|
+
<div
|
|
69
|
+
class=${classMap({
|
|
70
|
+
"fw-sc-layer-item": true,
|
|
71
|
+
"fw-sc-layer-item--selected": layer.id === this.selectedLayerId,
|
|
72
|
+
"fw-sc-layer-item--dragging": layer.id === this._draggedId,
|
|
73
|
+
"fw-sc-layer-item--drag-over": layer.id === this._dragOverId,
|
|
74
|
+
"fw-sc-layer-item--hidden": !layer.visible,
|
|
75
|
+
})}
|
|
76
|
+
draggable="true"
|
|
77
|
+
@dragstart=${(e: DragEvent) => this._handleDragStart(e, layer.id)}
|
|
78
|
+
@dragover=${(e: DragEvent) => this._handleDragOver(e, layer.id)}
|
|
79
|
+
@dragleave=${() => {
|
|
80
|
+
this._dragOverId = null;
|
|
81
|
+
}}
|
|
82
|
+
@drop=${(e: DragEvent) => this._handleDrop(e, layer.id)}
|
|
83
|
+
@dragend=${() => {
|
|
84
|
+
this._draggedId = null;
|
|
85
|
+
this._dragOverId = null;
|
|
86
|
+
}}
|
|
87
|
+
@click=${() =>
|
|
88
|
+
this._dispatch("fw-sc-layer-select", {
|
|
89
|
+
layerId: layer.id === this.selectedLayerId ? null : layer.id,
|
|
90
|
+
})}
|
|
91
|
+
>
|
|
92
|
+
<button
|
|
93
|
+
class=${classMap({
|
|
94
|
+
"fw-sc-layer-visibility": true,
|
|
95
|
+
"fw-sc-layer-visibility--visible": layer.visible,
|
|
96
|
+
})}
|
|
97
|
+
@click=${(e: Event) => {
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
this._dispatch("fw-sc-visibility-toggle", {
|
|
100
|
+
layerId: layer.id,
|
|
101
|
+
visible: !layer.visible,
|
|
102
|
+
});
|
|
103
|
+
}}
|
|
104
|
+
title=${layer.visible ? "Hide layer" : "Show layer"}
|
|
105
|
+
>
|
|
106
|
+
${layer.visible ? eyeIcon(14) : eyeOffIcon(14)}
|
|
107
|
+
</button>
|
|
108
|
+
<span class="fw-sc-layer-icon">${this._getSourceIcon(layer.sourceId)}</span>
|
|
109
|
+
<span class="fw-sc-layer-name">${this._getSourceLabel(layer.sourceId)}</span>
|
|
110
|
+
|
|
111
|
+
${this._editingLayerId === layer.id
|
|
112
|
+
? html`
|
|
113
|
+
<div class="fw-sc-layer-opacity">
|
|
114
|
+
<input
|
|
115
|
+
type="range"
|
|
116
|
+
min="0"
|
|
117
|
+
max="1"
|
|
118
|
+
step="0.1"
|
|
119
|
+
.value=${String(layer.transform.opacity)}
|
|
120
|
+
@input=${(e: Event) =>
|
|
121
|
+
this._dispatch("fw-sc-transform-edit", {
|
|
122
|
+
layerId: layer.id,
|
|
123
|
+
transform: {
|
|
124
|
+
opacity: Number((e.target as HTMLInputElement).value),
|
|
125
|
+
},
|
|
126
|
+
})}
|
|
127
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
128
|
+
/>
|
|
129
|
+
<span>${Math.round(layer.transform.opacity * 100)}%</span>
|
|
130
|
+
</div>
|
|
131
|
+
`
|
|
132
|
+
: nothing}
|
|
133
|
+
|
|
134
|
+
<div class="fw-sc-layer-controls">
|
|
135
|
+
<button
|
|
136
|
+
class="fw-sc-layer-btn"
|
|
137
|
+
@click=${(e: Event) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
this._moveUp(layer.id);
|
|
140
|
+
}}
|
|
141
|
+
?disabled=${index === 0}
|
|
142
|
+
title="Move up"
|
|
143
|
+
>
|
|
144
|
+
↑
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
class="fw-sc-layer-btn"
|
|
148
|
+
@click=${(e: Event) => {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
this._moveDown(layer.id);
|
|
151
|
+
}}
|
|
152
|
+
?disabled=${index === sorted.length - 1}
|
|
153
|
+
title="Move down"
|
|
154
|
+
>
|
|
155
|
+
↓
|
|
156
|
+
</button>
|
|
157
|
+
<button
|
|
158
|
+
class=${classMap({
|
|
159
|
+
"fw-sc-layer-btn": true,
|
|
160
|
+
"fw-sc-layer-btn--active": this._editingLayerId === layer.id,
|
|
161
|
+
})}
|
|
162
|
+
@click=${(e: Event) => {
|
|
163
|
+
e.stopPropagation();
|
|
164
|
+
this._editingLayerId =
|
|
165
|
+
this._editingLayerId === layer.id ? null : layer.id;
|
|
166
|
+
}}
|
|
167
|
+
title="Edit opacity"
|
|
168
|
+
>
|
|
169
|
+
⚙
|
|
170
|
+
</button>
|
|
171
|
+
<button
|
|
172
|
+
class="fw-sc-layer-btn fw-sc-layer-btn--danger"
|
|
173
|
+
@click=${(e: Event) => {
|
|
174
|
+
e.stopPropagation();
|
|
175
|
+
this._dispatch("fw-sc-layer-remove", { layerId: layer.id });
|
|
176
|
+
}}
|
|
177
|
+
title="Remove layer"
|
|
178
|
+
>
|
|
179
|
+
×
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
`
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private _dispatch(name: string, detail: unknown) {
|
|
191
|
+
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private _handleDragStart(e: DragEvent, layerId: string) {
|
|
195
|
+
this._draggedId = layerId;
|
|
196
|
+
e.dataTransfer!.effectAllowed = "move";
|
|
197
|
+
e.dataTransfer!.setData("text/plain", layerId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _handleDragOver(e: DragEvent, layerId: string) {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
e.dataTransfer!.dropEffect = "move";
|
|
203
|
+
this._dragOverId = layerId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private _handleDrop(e: DragEvent, targetLayerId: string) {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
this._dragOverId = null;
|
|
209
|
+
if (!this._draggedId || this._draggedId === targetLayerId) {
|
|
210
|
+
this._draggedId = null;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const sorted = this._sortedLayers;
|
|
214
|
+
const currentIds = sorted.map((l) => l.id);
|
|
215
|
+
const fromIndex = currentIds.indexOf(this._draggedId);
|
|
216
|
+
const toIndex = currentIds.indexOf(targetLayerId);
|
|
217
|
+
if (fromIndex === -1 || toIndex === -1) {
|
|
218
|
+
this._draggedId = null;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const newOrder = [...currentIds];
|
|
222
|
+
newOrder.splice(fromIndex, 1);
|
|
223
|
+
newOrder.splice(toIndex, 0, this._draggedId);
|
|
224
|
+
this._dispatch("fw-sc-reorder", { layerIds: newOrder });
|
|
225
|
+
this._draggedId = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private _moveUp(layerId: string) {
|
|
229
|
+
const sorted = this._sortedLayers;
|
|
230
|
+
const ids = sorted.map((l) => l.id);
|
|
231
|
+
const idx = ids.indexOf(layerId);
|
|
232
|
+
if (idx <= 0) return;
|
|
233
|
+
[ids[idx - 1], ids[idx]] = [ids[idx], ids[idx - 1]];
|
|
234
|
+
this._dispatch("fw-sc-reorder", { layerIds: ids });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private _moveDown(layerId: string) {
|
|
238
|
+
const sorted = this._sortedLayers;
|
|
239
|
+
const ids = sorted.map((l) => l.id);
|
|
240
|
+
const idx = ids.indexOf(layerId);
|
|
241
|
+
if (idx >= ids.length - 1) return;
|
|
242
|
+
[ids[idx], ids[idx + 1]] = [ids[idx + 1], ids[idx]];
|
|
243
|
+
this._dispatch("fw-sc-reorder", { layerIds: ids });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
declare global {
|
|
248
|
+
interface HTMLElementTagNameMap {
|
|
249
|
+
"fw-sc-layer-list": FwScLayerList;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <fw-sc-scene-switcher> — Horizontal scene selector with transition controls.
|
|
3
|
+
* Port of SceneSwitcher.tsx from streamcrafter-react.
|
|
4
|
+
*/
|
|
5
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
6
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
7
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
8
|
+
import { sharedStyles } from "../styles/shared-styles.js";
|
|
9
|
+
import { utilityStyles } from "../styles/utility-styles.js";
|
|
10
|
+
import type {
|
|
11
|
+
Scene,
|
|
12
|
+
TransitionConfig,
|
|
13
|
+
TransitionType,
|
|
14
|
+
} from "@livepeer-frameworks/streamcrafter-core";
|
|
15
|
+
|
|
16
|
+
@customElement("fw-sc-scene-switcher")
|
|
17
|
+
export class FwScSceneSwitcher extends LitElement {
|
|
18
|
+
@property({ attribute: false }) scenes: Scene[] = [];
|
|
19
|
+
@property({ type: String, attribute: "active-scene-id" }) activeSceneId: string | null = null;
|
|
20
|
+
@property({ type: Boolean, attribute: "show-transition-controls" }) showTransitionControls = true;
|
|
21
|
+
|
|
22
|
+
@state() private _selectedTransition: TransitionType = "fade";
|
|
23
|
+
@state() private _transitionDuration = 500;
|
|
24
|
+
@state() private _isTransitioning = false;
|
|
25
|
+
|
|
26
|
+
static styles = [
|
|
27
|
+
sharedStyles,
|
|
28
|
+
utilityStyles,
|
|
29
|
+
css`
|
|
30
|
+
:host {
|
|
31
|
+
display: block;
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
protected render() {
|
|
37
|
+
return html`
|
|
38
|
+
<div class="fw-sc-scene-switcher">
|
|
39
|
+
<div class="fw-sc-scene-switcher-header">
|
|
40
|
+
<span class="fw-sc-scene-switcher-title">Scenes</span>
|
|
41
|
+
${this.showTransitionControls
|
|
42
|
+
? html`
|
|
43
|
+
<div class="fw-sc-transition-controls">
|
|
44
|
+
<select
|
|
45
|
+
class="fw-sc-transition-select"
|
|
46
|
+
.value=${this._selectedTransition}
|
|
47
|
+
@change=${(e: Event) => {
|
|
48
|
+
this._selectedTransition = (e.target as HTMLSelectElement)
|
|
49
|
+
.value as TransitionType;
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<option value="cut">Cut</option>
|
|
53
|
+
<option value="fade">Fade</option>
|
|
54
|
+
<option value="slide-left">Slide Left</option>
|
|
55
|
+
<option value="slide-right">Slide Right</option>
|
|
56
|
+
<option value="slide-up">Slide Up</option>
|
|
57
|
+
<option value="slide-down">Slide Down</option>
|
|
58
|
+
</select>
|
|
59
|
+
<input
|
|
60
|
+
type="number"
|
|
61
|
+
class="fw-sc-transition-duration"
|
|
62
|
+
.value=${String(this._transitionDuration)}
|
|
63
|
+
@change=${(e: Event) => {
|
|
64
|
+
this._transitionDuration = Number((e.target as HTMLInputElement).value);
|
|
65
|
+
}}
|
|
66
|
+
min="0"
|
|
67
|
+
max="3000"
|
|
68
|
+
step="100"
|
|
69
|
+
title="Transition duration (ms)"
|
|
70
|
+
/>
|
|
71
|
+
<span class="fw-sc-transition-unit">ms</span>
|
|
72
|
+
</div>
|
|
73
|
+
`
|
|
74
|
+
: nothing}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="fw-sc-scene-list">
|
|
78
|
+
${this.scenes.map(
|
|
79
|
+
(scene) => html`
|
|
80
|
+
<div
|
|
81
|
+
class=${classMap({
|
|
82
|
+
"fw-sc-scene-item": true,
|
|
83
|
+
"fw-sc-scene-item--active": scene.id === this.activeSceneId,
|
|
84
|
+
"fw-sc-scene-item--transitioning": this._isTransitioning,
|
|
85
|
+
})}
|
|
86
|
+
@click=${() => this._handleSceneClick(scene.id)}
|
|
87
|
+
style="background-color:${scene.backgroundColor}"
|
|
88
|
+
>
|
|
89
|
+
<span class="fw-sc-scene-name">${scene.name}</span>
|
|
90
|
+
<span class="fw-sc-scene-layer-count">${scene.layers.length} layers</span>
|
|
91
|
+
${this.scenes.length > 1 && scene.id !== this.activeSceneId
|
|
92
|
+
? html`
|
|
93
|
+
<button
|
|
94
|
+
class="fw-sc-scene-delete"
|
|
95
|
+
@click=${(e: Event) => {
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
this._handleDelete(scene.id);
|
|
98
|
+
}}
|
|
99
|
+
title="Delete scene"
|
|
100
|
+
>
|
|
101
|
+
×
|
|
102
|
+
</button>
|
|
103
|
+
`
|
|
104
|
+
: nothing}
|
|
105
|
+
</div>
|
|
106
|
+
`
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
<button
|
|
110
|
+
class="fw-sc-scene-add"
|
|
111
|
+
@click=${() =>
|
|
112
|
+
this.dispatchEvent(
|
|
113
|
+
new CustomEvent("fw-sc-scene-create", { bubbles: true, composed: true })
|
|
114
|
+
)}
|
|
115
|
+
title="Create new scene"
|
|
116
|
+
>
|
|
117
|
+
+
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async _handleSceneClick(sceneId: string) {
|
|
125
|
+
if (sceneId === this.activeSceneId || this._isTransitioning) return;
|
|
126
|
+
this._isTransitioning = true;
|
|
127
|
+
try {
|
|
128
|
+
this.dispatchEvent(
|
|
129
|
+
new CustomEvent("fw-sc-scene-select", {
|
|
130
|
+
detail: {
|
|
131
|
+
sceneId,
|
|
132
|
+
transition: {
|
|
133
|
+
type: this._selectedTransition,
|
|
134
|
+
durationMs: this._transitionDuration,
|
|
135
|
+
easing: "ease-in-out" as const,
|
|
136
|
+
} satisfies TransitionConfig,
|
|
137
|
+
},
|
|
138
|
+
bubbles: true,
|
|
139
|
+
composed: true,
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
} finally {
|
|
143
|
+
this._isTransitioning = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private _handleDelete(sceneId: string) {
|
|
148
|
+
if (this.scenes.length <= 1) return;
|
|
149
|
+
this.dispatchEvent(
|
|
150
|
+
new CustomEvent("fw-sc-scene-delete", {
|
|
151
|
+
detail: { sceneId },
|
|
152
|
+
bubbles: true,
|
|
153
|
+
composed: true,
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
declare global {
|
|
160
|
+
interface HTMLElementTagNameMap {
|
|
161
|
+
"fw-sc-scene-switcher": FwScSceneSwitcher;
|
|
162
|
+
}
|
|
163
|
+
}
|