Skip to content

Commit

Permalink
重制视频画面滤镜设置部分
Browse files Browse the repository at this point in the history
  • Loading branch information
otomad committed Jan 11, 2024
1 parent 798cb81 commit 69da6ca
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 67 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ module.exports = {
"default-case-last": "off",
"no-useless-constructor": "off", // private constructor() { } 你跟我说无用?
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 0, maxBOF: 0 }],
"no-unused-expressions": ["error", {
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
enforceForJSX: true,
}],
"import/order": "off", // 与 VSCode 内置导入排序特性打架。
"import/first": "off", // 与 Vue 特性冲突。
"import/named": "off", // 与 TypeScript 特性冲突。
Expand Down
6 changes: 6 additions & 0 deletions assets/styles/theme/_effects.scss
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
@mixin acrylic-background {
background-color: c(acrylic-bg, 75%);
}

@mixin accent-ripple {
:deep(.ripple-circle) {
background-color: c(accent-ripple);
}
}
// #endregion

// #region 主要主题色
Expand Down
6 changes: 2 additions & 4 deletions components/Accordion/AccordionItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,11 @@
}
html:not(.dark) & {
@include accent-ripple;
.right :deep(.soft-button) {
--active: true;
}
:deep(.ripple-circle) {
background-color: c(accent-ripple);
}
}
}
}
Expand Down
17 changes: 7 additions & 10 deletions components/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
button[disabled] > & {
background-color: c(accent-disabled) !important;
box-shadow: none !important;
html.dark & :is(.caption, .icon) {
opacity: 0.4;
}
Expand Down Expand Up @@ -136,14 +136,11 @@
color: inherit;
}
}
@container style(--appearance: secondary) {
@include accent-ripple;
color: c(accent);
:deep(.ripple-circle) {
background-color: c(accent-ripple);
}
button:any-hover > &,
button:active > & {
background-color: c(accent-hover-overlay);
Expand All @@ -161,7 +158,7 @@
@container style(--appearance: tertiary) {
color: c(icon-color);
button:any-hover > &,
button:active > & {
background-color: c(hover-overlay);
Expand Down Expand Up @@ -237,7 +234,7 @@
button.icon-behind {
--icon-behind: true;
}
.progress-bar:deep {
position: absolute !important;
bottom: 0;
Expand All @@ -248,11 +245,11 @@
translate: -50%;
background-color: transparent;
}
button:not([disabled]) & .line {
background-color: white;
}
&.v-enter-from,
&.v-leave-to {
height: 0;
Expand Down
47 changes: 40 additions & 7 deletions components/CheckCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@

<template>
<Comp role="checkbox" :aria-checked="checked" :class="{ checked }" :tabindex="0" @click="checked = !checked">
<div class="card">
<img :src="image" />
<div v-ripple class="card">
<slot name="image"><img :src="image" /></slot>
<div class="overlay"></div>
<div class="float">
<Checkbox v-model:single="checked" readonly />
</div>
Expand All @@ -32,9 +33,13 @@
--width: 208px;
/// 卡片高度。
--height: 117px;
/// 卡片宽高比。
--aspect-ratio: auto;
/// 复选框到卡片左上角的距离。
--float-offset: 12px;
}
}
:comp {
display: inline-block;
}
Expand All @@ -44,6 +49,7 @@
position: relative;
width: var(--width);
height: var(--height);
aspect-ratio: var(--aspect-ratio);
background-color: c(gray-20);
outline: 1px solid transparent;
cursor: pointer;
Expand All @@ -55,21 +61,48 @@
:comp:active & {
@include button-scale-pressed;
}
:comp.checked & {
@include accent-ripple;
outline-color: c(accent);
}
.float {
@include round-small;
@include card-shadow-with-blur;
position: absolute;
top: 12px;
left: 12px;
top: var(--float-offset);
left: var(--float-offset);
z-index: 5;
padding: 11px;
background-color: c(main-bg, 50%);
pointer-events: none;
:comp:not(.checked, :any-hover, :focus-visible) & {
opacity: 0;
}
}
.overlay {
@include square(100%);
position: absolute;
top: 0;
left: 0;
background-color: c(accent, 40%);
:comp:not(.checked) & {
opacity: 0;
}
}
&:deep(.ripple-circle) {
z-index: 4;
}
> :deep(img) {
@include square(100%);
object-fit: cover;
}
}
.text {
Expand Down
29 changes: 26 additions & 3 deletions components/Player/PlayerVideo/PlayerVideo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,35 @@
grayscale: false,
invert: false,
sepia: false,
brightness: 1,
contrast: 1,
saturate: 1,
hue: 0,
saturate: 1,
contrast: 1,
brightness: 1,
},
});
const videoFilterStyle = computed(() => {
const { filter } = settings;
const style: CSSProperties = {};
if (filter.horizontalFlip || filter.verticalFlip) {
const scale: TwoD = [1, 1];
if (filter.horizontalFlip) scale[0] = -1;
if (filter.verticalFlip) scale[1] = -1;
style.scale = scale.join(" ");
}
if (filter.rotation) style.rotate = filter.rotation + "deg";
const filters: string[] = [];
if (filter.grayscale) filters.push("grayscale(1)");
if (filter.invert) filters.push("invert(1)");
if (filter.sepia) filters.push("sepia(1)");
if (filter.hue % 360 !== 0) filters.push(`hue-rotate(${filter.hue}deg)`);
if (filter.saturate !== 1) filters.push(`saturate(${filter.saturate})`);
if (filter.contrast !== 1) filters.push(`contrast(${filter.contrast})`);
if (filter.brightness !== 1) filters.push(`brightness(${filter.brightness})`);
if (filters.length) style.filter = filters.join(" ");
return style;
});
const qualities = ref<BitrateInfo[]>([]);
const mediaInfos = ref<MediaInfo>();
const videoContainer = ref<HTMLDivElement>();
Expand Down Expand Up @@ -338,6 +360,7 @@
<video
ref="video"
class="player"
:style="videoFilterStyle"
@play="playing = true"
@pause="playing = false"
@ratechange="e => playbackRate = (e.target as HTMLVideoElement).playbackRate"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,97 @@
/** 视频播放器设置。 */
settings: PlayerVideoSettings;
}>();
</script>
<template>
<Comp>
<ShadingIcon icon="settings" position="right bottom" rotating :elastic="playing" large />
type Filters = keyof PlayerVideoSettings["filter"] | "rotation90" | "rotation180" | "rotation270";
<p>弹幕</p>
<SettingsSlider
v-model="settings.danmaku.fontSizeScale"
:min="0"
:max="2"
:defaultValue="1"
icon="font_size"
>字号缩放</SettingsSlider>
<SettingsSlider
v-model="settings.danmaku.opacity"
:min="0"
:max="1"
:defaultValue="1"
icon="opacity"
>不透明度</SettingsSlider>
const filters: Record<Exclude<Filters, "rotation">, [string, CSSProperties]> = {
horizontalFlip: ["水平翻转", { scale: "-1 1" }],
verticalFlip: ["垂直翻转", { scale: "1 -1" }],
rotation90: ["旋转90°", { rotate: "90deg" }],
rotation180: ["旋转180°", { rotate: "180deg" }],
rotation270: ["旋转270°", { rotate: "270deg" }],
grayscale: ["黑白", { filter: "grayscale(1)" }],
invert: ["反转", { filter: "invert(1)" }],
sepia: ["怀旧", { filter: "sepia(1)" }],
hue: ["调整色相", { filter: "hue-rotate(180deg)" }],
saturate: ["调整饱和度", { filter: "saturate(5)" }],
contrast: ["调整对比度", { filter: "contrast(5)" }],
brightness: ["调整亮度", { filter: "brightness(2)" }],
};
<p>滤镜</p>
<CheckCard v-model="settings.filter.horizontalFlip">水平翻转</CheckCard>
<ToggleSwitch v-model="settings.filter.horizontalFlip" v-ripple icon="flip_horizontal">水平翻转</ToggleSwitch>
<ToggleSwitch v-model="settings.filter.verticalFlip" v-ripple icon="flip_vertical">垂直翻转</ToggleSwitch>
const filterBooleanProxy = new Proxy({}, {
get(_target, prop: Filters) {
const propOriginal = (prop.startsWith("rotation") ? "rotation" : prop) as keyof PlayerVideoSettings["filter"];
const value = props.settings.filter[propOriginal];
return ({
rotation90: value === 90,
rotation180: value === 180,
rotation270: value === 270,
hue: value as number % 360 !== 0,
saturate: value !== 1,
contrast: value !== 1,
brightness: value !== 1,
} as Record<Filters, boolean>)[prop] ?? value as boolean;
},
set(_target, prop: Filters, newValue: boolean) {
const { filter } = props.settings;
if (prop.startsWith("rotation")) {
if (!newValue) filter.rotation = 0;
else {
const rotation = +prop.match(/\d+$/)![0];
filter.rotation = rotation as never;
}
return true;
}
/* eslint-disable indent */
prop === "hue" ? (filter.hue = newValue ? 180 : 0) :
prop === "saturate" ? (filter.saturate = newValue ? 5 : 1) :
prop === "contrast" ? (filter.contrast = newValue ? 5 : 1) :
prop === "brightness" ? (filter.brightness = newValue ? 2 : 1) :
filter[prop as never] = newValue as never;
/* eslint-enable indent */
return true;
},
}) as Record<Filters, boolean>;
</script>

</Comp>
<template>
<div class="wrapper">
<Comp>
<p>弹幕</p>
<SettingsSlider
v-model="settings.danmaku.fontSizeScale"
:min="0"
:max="2"
:defaultValue="1"
icon="font_size"
>字号缩放</SettingsSlider>
<SettingsSlider
v-model="settings.danmaku.opacity"
:min="0"
:max="1"
:defaultValue="1"
icon="opacity"
>不透明度</SettingsSlider>

<p>滤镜</p>
<div class="grid">
<CheckCard v-for="([filter, style], key) in filters" :key="key" v-model="filterBooleanProxy[key]">
{{ filter }}
<template #image><img :src="thumbnail" alt="preview" :style="style" /></template>
</CheckCard>
<!-- <CheckCard v-model="settings.filter.horizontalFlip">
水平翻转
<template #image><img :src="thumbnail" alt="preview" /></template>
</CheckCard>
<CheckCard v-model="settings.filter.verticalFlip">
垂直翻转
<template #image><img :src="thumbnail" alt="preview" /></template>
</CheckCard> -->
</div>
</Comp>
<ShadingIcon icon="settings" position="right bottom" rotating :elastic="playing" large />
</div>
</template>

<style scoped lang="scss">
Expand All @@ -45,7 +108,15 @@
:comp {
position: relative;
flex-grow: 1;
height: 100%;
padding-top: 8px;
contain: strict;
overflow: hidden auto;
}
.wrapper {
@include square(100%);
position: relative;
}
.shading-icon {
Expand All @@ -61,6 +132,20 @@
font-weight: 600;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 16px;
.check-card {
--width: 100%;
--height: unset;
--aspect-ratio: 1 / 1;
--float-offset: 6px;
}
}
.toggle-switch {
height: 48px;
padding: 0 $padding;
Expand Down
Loading

0 comments on commit 69da6ca

Please sign in to comment.