Skip to content

Commit

Permalink
✨ Edge blending
Browse files Browse the repository at this point in the history
- Search for CCLI number
- Text will use original color if output has a template
  • Loading branch information
vassbo committed Dec 12, 2024
1 parent 0f2d7b3 commit f0a150c
Show file tree
Hide file tree
Showing 19 changed files with 200 additions and 62 deletions.
6 changes: 5 additions & 1 deletion public/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
"top": "Top",
"right": "Right",
"bottom": "Bottom",
"left": "Left"
"left": "Left",
"centered": "Centered",
"edge_blending_tip": "Blend together multiple outputs/projectors for a more seamless transition"
},
"about": {
"check_updates": "Look for updates",
Expand Down Expand Up @@ -849,6 +851,7 @@
"outline": "Outline",
"shadow": "Shadow",
"shadow_inset": "Shadow inset",
"offset": "Offset",
"offsetX": "Offset X",
"offsetY": "Offset Y",
"blur": "Blur",
Expand Down Expand Up @@ -1067,6 +1070,7 @@
"manual_input_hint": "Can't find the display? Click here to manually change the position.",
"manual_drag_hint": "You can also hold ctrl/cmd over an active output window to manually drag it around.",
"allow_main_screen": "Allow custom output position & size",
"edge_blending": "Edge blending",
"identify_screens": "Identify screens",
"new_output": "New output",
"normal": "Normal",
Expand Down
9 changes: 7 additions & 2 deletions src/frontend/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ContextMenu from "./components/context/ContextMenu.svelte"
import Pdf from "./components/export/Pdf.svelte"
import Guide from "./components/guide/Guide.svelte"
import { getBlending } from "./components/helpers/output"
import { startEventTimer, startTimer } from "./components/helpers/timerTick"
import Loader from "./components/main/Loader.svelte"
import MenuBar from "./components/main/MenuBar.svelte"
Expand All @@ -12,7 +13,7 @@
import Toast from "./components/main/Toast.svelte"
import QuickSearch from "./components/quicksearch/QuickSearch.svelte"
import Center from "./components/system/Center.svelte"
import { activeTimers, autosave, closeAd, currentWindow, disabledServers, events, loaded, os, outputDisplay } from "./stores"
import { activeTimers, autosave, closeAd, currentWindow, disabledServers, events, loaded, os, outputDisplay, outputs } from "./stores"
import { focusArea, logerror, startAutosave, toggleRemoteStream } from "./utils/common"
import { keydown } from "./utils/shortcuts"
import { startup } from "./utils/startup"
Expand All @@ -37,6 +38,10 @@
// close youtube ad
$: if ($closeAd) setTimeout(() => closeAd.set(false), 10)
// edge blending
let blending = ""
$: if ($currentWindow === "output" && Object.values($outputs)[0]?.blending) blending = getBlending()
</script>

<svelte:window on:keydown={keydown} on:mousedown={focusArea} on:error={logerror} on:unhandledrejection={logerror} />
Expand All @@ -49,7 +54,7 @@
<MenuBar />
{/if}

<main style={isWindows ? "height: calc(100% - 25px);" : ""} class:closeAd={$closeAd} class:background={$currentWindow === "output"}>
<main style="{isWindows ? 'height: calc(100% - 25px);' : ''}{blending}" class:closeAd={$closeAd} class:background={$currentWindow === "output"}>
<ContextMenu />

{#if $currentWindow === "output"}
Expand Down
26 changes: 24 additions & 2 deletions src/frontend/components/helpers/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { getExtension, getFileName, removeExtension } from "./media"
import { replaceDynamicValues } from "./showActions"
import { _show } from "./shows"
import { newToast } from "../../utils/common"
import { getStyles } from "./style"

export function displayOutputs(e: any = {}, auto: boolean = false) {
// sort so display order can be changed! (needs app restart)
Expand All @@ -65,7 +66,7 @@ export function setOutput(key: string, data: any, toggle: boolean = false, outpu
let outs = outputId ? [outputId] : allOutputs
let inputData = clone(data)

let firstOutputWithBackground = allOutputs.findIndex((id) => (get(styles)[get(outputs)[id]?.style || ""]?.layers || ["background"]).includes("background"))
let firstOutputWithBackground = allOutputs.findIndex((id) => !a[id]?.isKeyOutput && !a[id]?.stageOutput && (get(styles)[get(outputs)[id]?.style || ""]?.layers || ["background"]).includes("background"))
firstOutputWithBackground = Math.max(0, firstOutputWithBackground)

// reset slide cache (after update)
Expand Down Expand Up @@ -570,7 +571,16 @@ export function mergeWithTemplate(slideItems: Item[], templateItems: Item[], add
line.align = templateLine?.align || ""
line.text?.forEach((text: any, k: number) => {
let templateText = templateLine?.text[k] || templateLine?.text[0]
if (!text.customType?.includes("disableTemplate")) text.style = templateText?.style || ""
if (!text.customType?.includes("disableTemplate")) {
let style = templateText?.style || ""

// add original text color
let textColor = getStyles(text.style)["color"] || "#FFFFFF"
// use template color if text is white (default)
if (textColor !== "#FFFFFF") style += `color: ${textColor};`

text.style = style
}

let firstChar = templateText?.value?.[0] || ""

Expand Down Expand Up @@ -930,3 +940,15 @@ export function getSlideFilter(slideData: any) {

return slideFilter
}

export function getBlending() {
let blending = Object.values(get(outputs))[0]?.blending
if (!blending) return ""

if (!blending.left && !blending.right) return ""

const opacity = (blending.opacity ?? 50) / 100
const center = 50 + Number(blending.offset || 0)
if (blending.centered) return `-webkit-mask-image: linear-gradient(${blending.rotate ?? 90}deg, rgb(0, 0, 0) ${center - blending.left}%, rgba(0, 0, 0, ${opacity}) ${center}%, rgb(0, 0, 0) ${center + Number(blending.right)}%);`
return `-webkit-mask-image: linear-gradient(${blending.rotate ?? 90}deg, rgba(0, 0, 0, ${opacity}) 0%, rgb(0, 0, 0) ${blending.left}%, rgb(0, 0, 0) ${100 - blending.right}%, rgba(0, 0, 0, ${opacity}) 100%);`
}
17 changes: 17 additions & 0 deletions src/frontend/components/helpers/setShow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getShowCacheId, updateCachedShow } from "./show"
import { uid } from "uid"
import { destroy } from "../../utils/request"
import { fixShowIssues } from "../../converters/importHelpers"
import type { ShowObj } from "../../classes/Show"

export function setShow(id: string, value: "delete" | Show): Show {
let previousValue: Show
Expand All @@ -27,6 +28,12 @@ export function setShow(id: string, value: "delete" | Show): Show {
if (!value.slides) value.slides = {}
if (!value.layouts) value.layouts = {}
if (!value.media) value.media = {}

// set metadata (CCLI) in quickAccess
if (value.meta.CCLI && !value.quickAccess?.metadata?.CCLI) {
if (!value.quickAccess?.metadata) value.quickAccess.metadata = {}
value.quickAccess.metadata.CCLI = value.meta.CCLI
}
}
}

Expand Down Expand Up @@ -174,3 +181,13 @@ export function saveTextCache(id: string, show: Show) {
tempCache = {}
}, 1000)
}

export function setQuickAccessMetadata(show: ShowObj, key: string, value: string) {
if (!value) return show

if (!show.quickAccess) show.quickAccess = {}
if (!show.quickAccess.metadata) show.quickAccess.metadata = {}
show.quickAccess.metadata[key] = value

return show
}
7 changes: 5 additions & 2 deletions src/frontend/components/inputs/ProjectButton.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { uid } from "uid"
import type { ID } from "../../../types/Show"
import { activeProject, activeShow, dictionary, projects, projectTemplates, projectView, saved, showRecentlyUsedProjects } from "../../stores"
import { activeProject, activeRename, activeShow, dictionary, projects, projectTemplates, projectView, saved, showRecentlyUsedProjects } from "../../stores"
import { clone } from "../helpers/array"
import { history } from "../helpers/history"
import Icon from "../helpers/Icon.svelte"
Expand Down Expand Up @@ -28,7 +29,9 @@
if (!project) return
project.parent = $projects[$activeProject || ""]?.parent || "/"
history({ id: "UPDATE", newData: { data: project }, location: { page: "show", id: "project" } })
let projectId = uid()
history({ id: "UPDATE", newData: { data: project }, oldData: { id: projectId }, location: { page: "show", id: "project" } })
setTimeout(() => activeRename.set("project_" + projectId))
return
}
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/components/output/Output.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@

<!-- colorbars for testing -->
{#if $colorbars}
<Image path="./assets/{$colorbars}" mediaStyle={{ rendering: "pixelated" }} />
<Image path="./assets/{$colorbars}" mediaStyle={{ rendering: "pixelated", fit: "fill" }} />
{/if}

<!-- "underlays" -->
Expand Down
137 changes: 98 additions & 39 deletions src/frontend/components/settings/Screens.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<script lang="ts">
import { onDestroy } from "svelte"
import { MAIN, OUTPUT } from "../../../types/Channels"
import { activePopup, alertMessage, currentOutputSettings, outputDisplay, outputs } from "../../stores"
import { activePopup, alertMessage, currentOutputSettings, dictionary, outputDisplay, outputs } from "../../stores"
import { destroy, receive, send } from "../../utils/request"
import Icon from "../helpers/Icon.svelte"
import T from "../helpers/T.svelte"
import Button from "../inputs/Button.svelte"
import { keysToID, sortByName } from "../helpers/array"
import CombinedInput from "../inputs/CombinedInput.svelte"
import NumberInput from "../inputs/NumberInput.svelte"
import Checkbox from "../inputs/Checkbox.svelte"
export let activateOutput: boolean = false
Expand Down Expand Up @@ -142,48 +145,104 @@
function identifyScreens() {
send(OUTPUT, ["IDENTIFY_SCREENS"], screens)
}
const isChecked = (e: any) => e.target.checked
let edgeBlending: boolean = false
$: blending = currentScreen.blending || {}
function updateBlending(value: number, side: string) {
if (!screenId) return
outputs.update((a) => {
if (!a[screenId].blending) a[screenId].blending = { left: 0, right: 0, rotate: 90, opacity: 50, centered: false, offset: 0 }
a[screenId].blending![side] = value
return a
})
}
</script>

<p style="margin-bottom: 10px;"><T id="settings.select_display" /></p>
<Button on:click={() => activePopup.set("change_output_values")} style="width: 100%;" dark center>
<Icon id="screen" right />
<p><T id="settings.manual_input_hint" /></p>
</Button>

<Button on:click={identifyScreens} style="width: 100%;" dark center>
<Icon id="search" right />
<p><T id="settings.identify_screens" /></p>
</Button>

<br />

<div class="content">
{#if screens.length}
<div class="screens" style="transform: translateX(-{totalScreensWidth}px)">
{#if !currentScreen.screen || !screens.find((a) => a.id.toString() === currentScreen.screen)}
<div class="screen noClick" style="width: {currentScreen.bounds.width}px;height: {currentScreen.bounds.height}px;left: {currentScreen.bounds.x}px;top: {currentScreen.bounds.y}px;">
<!-- Current screen position -->
</div>
{/if}

{#each screens as screen, i}
<div
class="screen"
class:disabled={currentScreen?.forcedResolution}
class:active={$outputs[screenId || ""]?.screen === screen.id.toString()}
style="width: {screen.bounds.width}px;height: {screen.bounds.height}px;left: {screen.bounds.x}px;top: {screen.bounds.y}px;"
on:click={() => {
if (!currentScreen?.forcedResolution) changeOutputScreen({ detail: { id: screen.id, bounds: screen.bounds } })
}}
>
{i + 1}
</div>
{/each}
{#if edgeBlending}
<Button style="position: absolute;left: 0;top: 0;min-height: 58px;" title={$dictionary.actions?.back} on:click={() => (edgeBlending = false)}>
<Icon id="back" size={2} white />
</Button>

<p style="margin-bottom: 10px;"><T id="screen.edge_blending_tip" /></p>

<CombinedInput>
<p><T id="screen.left" /></p>
<NumberInput value={blending.left || 0} on:change={(e) => updateBlending(e.detail, "left")} />
</CombinedInput>
<CombinedInput>
<p><T id="screen.right" /></p>
<NumberInput value={blending.right || 0} on:change={(e) => updateBlending(e.detail, "right")} />
</CombinedInput>
<CombinedInput>
<p><T id="edit.rotation" /></p>
<NumberInput value={blending.rotate ?? 90} max={360} on:change={(e) => updateBlending(e.detail, "rotate")} />
</CombinedInput>
<CombinedInput>
<p><T id="edit.opacity" /></p>
<NumberInput value={blending.opacity ?? 50} max={100} step={5} on:change={(e) => updateBlending(e.detail, "opacity")} />
</CombinedInput>
<CombinedInput>
<p><T id="screen.centered" /></p>
<div class="alignRight">
<Checkbox checked={blending.centered} on:change={(e) => updateBlending(isChecked(e), "centered")} />
</div>
{:else}
<T id="remote.loading" />
</CombinedInput>
{#if blending.centered}
<CombinedInput>
<p><T id="edit.offset" /></p>
<NumberInput value={blending.offset} min={-50} max={50} on:change={(e) => updateBlending(e.detail, "offset")} />
</CombinedInput>
{/if}
</div>
{:else}
<p style="margin-bottom: 10px;"><T id="settings.select_display" /></p>
<Button on:click={() => activePopup.set("change_output_values")} style="width: 100%;" dark center>
<Icon id="screen" right />
<p><T id="settings.manual_input_hint" /></p>
</Button>

<Button on:click={() => (edgeBlending = true)} style="width: 100%;" dark center>
<Icon id="gradient" right />
<p><T id="settings.edge_blending" /></p>
</Button>

<Button on:click={identifyScreens} style="width: 100%;" dark center>
<Icon id="search" right />
<p><T id="settings.identify_screens" /></p>
</Button>

<br />

<div class="content">
{#if screens.length}
<div class="screens" style="transform: translateX(-{totalScreensWidth}px)">
{#if !currentScreen.screen || !screens.find((a) => a.id.toString() === currentScreen.screen)}
<div class="screen noClick" style="width: {currentScreen.bounds.width}px;height: {currentScreen.bounds.height}px;left: {currentScreen.bounds.x}px;top: {currentScreen.bounds.y}px;">
<!-- Current screen position -->
</div>
{/if}

{#each screens as screen, i}
<div
class="screen"
class:disabled={currentScreen?.forcedResolution}
class:active={$outputs[screenId || ""]?.screen === screen.id.toString()}
style="width: {screen.bounds.width}px;height: {screen.bounds.height}px;left: {screen.bounds.x}px;top: {screen.bounds.y}px;"
on:click={() => {
if (!currentScreen?.forcedResolution) changeOutputScreen({ detail: { id: screen.id, bounds: screen.bounds } })
}}
>
{i + 1}
</div>
{/each}
</div>
{:else}
<T id="remote.loading" />
{/if}
</div>
{/if}

<style>
.content {
Expand Down
16 changes: 12 additions & 4 deletions src/frontend/components/show/tools/Metadata.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,16 @@
values[key] = e.target?.value || e.value || ""
updateData(values, "meta")
if (key === "number") {
const quickAccess = { ...($showsCache[$activeShow!.id].quickAccess || {}), number: values[key] }
const quickAccessKeys = ["number", "CCLI"]
if (quickAccessKeys.includes(key)) {
let quickAccess = $showsCache[$activeShow!.id].quickAccess || {}
if (key === "number") quickAccess.number = values.number
else {
if (!quickAccess.metadata) quickAccess.metadata = {}
quickAccess.metadata[key] = values[key]
}
showsCache.update((a) => {
a[$activeShow!.id].quickAccess = quickAccess
return a
Expand Down Expand Up @@ -127,9 +135,9 @@
{#each Object.entries(values) as [key, value]}
<CombinedInput textWidth={40}>
{#if $dictionary.meta?.[key]}
<p title={$dictionary.meta?.[key]}><T id="meta.{key}" /></p>
<p style="overflow: hidden;display: block;align-content: center;" title={$dictionary.meta?.[key]}><T id="meta.{key}" /></p>
{:else}
<p style="text-transform: capitalize;">{key}</p>
<p style="overflow: hidden;display: block;align-content: center;text-transform: capitalize;">{key}</p>
{/if}
<TextInput {value} style={key === "number" && currentShow?.quickAccess?.number ? "border-bottom: 1px solid var(--secondary);" : ""} on:change={(e) => changeValue(e, key)} />

Expand Down
Loading

0 comments on commit f0a150c

Please sign in to comment.