This commit is contained in:
syuilo 2025-06-03 16:55:14 +09:00
parent 368b98bbfd
commit 7bff6cccb7
5 changed files with 193 additions and 2 deletions

4
locales/index.d.ts vendored
View File

@ -12105,6 +12105,10 @@ export interface Locale extends ILocale {
*
*/
"dottedGrid": string;
/**
*
*/
"checker": string;
};
"_imageEffector": {
/**

View File

@ -3241,6 +3241,7 @@ _watermarkEditor:
stripeFrequency: "ラインの数"
angle: "角度"
dottedGrid: "点グリッド"
checker: "チェッカー"
_imageEffector:
title: "エフェクト"

View File

@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._watermarkEditor.repeat }}</template>
</MkSwitch>
</template>
<template v-else-if="layer.type === 'image'">
<MkButton inline rounded primary @click="chooseFile">{{ i18n.ts.selectFile }}</MkButton>
@ -105,6 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._watermarkEditor.cover }}</template>
</MkSwitch>
</template>
<template v-else-if="layer.type === 'stripe'">
<MkRange
v-model="layer.frequency"
@ -147,12 +149,109 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
</MkRange>
</template>
<template v-else-if="layer.type === 'dottedGrid'">
<MkRange
v-model="layer.angle"
:min="-1"
:max="1"
:step="0.01"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.angle }}</template>
</MkRange>
<MkRange
v-model="layer.scale"
:min="0"
:max="10"
:step="0.01"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
</MkRange>
<MkRange
v-model="layer.minorRadius"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.gridMinorRadius }}</template>
</MkRange>
<MkRange
v-model="layer.minorDivisions"
:min="0"
:max="16"
:step="1"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.gridMinorDivisions }}</template>
</MkRange>
<MkRange
v-model="layer.majorOpacity"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.gridMajorOpacity }}</template>
</MkRange>
<MkRange
v-model="layer.minorOpacity"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.gridMinorOpacity }}</template>
</MkRange>
</template>
<template v-else-if="layer.type === 'checker'">
<MkRange
v-model="layer.angle"
:min="-1"
:max="1"
:step="0.01"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.angle }}</template>
</MkRange>
<MkRange
v-model="layer.scale"
:min="0"
:max="10"
:step="0.01"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
</MkRange>
<MkRange
v-model="layer.opacity"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
</MkRange>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import type { WatermarkPreset } from '@/utility/watermark.js';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';

View File

@ -47,6 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
<div v-if="layer.type === 'dottedGrid'">{{ i18n.ts._watermarkEditor.dottedGrid }}</div>
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
</template>
<template #footer>
<div class="_buttons">
@ -127,6 +129,33 @@ function createStripeLayer(): WatermarkPreset['layers'][number] {
};
}
function createDottedGridLayer(): WatermarkPreset['layers'][number] {
return {
id: genId(),
type: 'dottedGrid',
angle: 0.5,
scale: 3,
majorRadius: 0.1,
minorRadius: 0.25,
majorOpacity: 0.75,
minorOpacity: 0.5,
minorDivisions: 4,
black: false,
opacity: 0.75,
};
}
function createCheckerLayer(): WatermarkPreset['layers'][number] {
return {
id: genId(),
type: 'checker',
angle: 0.5,
scale: 3,
black: false,
opacity: 0.75,
};
}
const props = defineProps<{
preset?: WatermarkPreset | null;
image?: File | null;
@ -301,6 +330,16 @@ function addLayer(ev: MouseEvent) {
action: () => {
preset.layers.push(createStripeLayer());
},
}, {
text: i18n.ts._watermarkEditor.dottedGrid,
action: () => {
preset.layers.push(createDottedGridLayer());
},
}, {
text: i18n.ts._watermarkEditor.checker,
action: () => {
preset.layers.push(createCheckerLayer());
},
}], ev.currentTarget ?? ev.target);
}

View File

@ -5,6 +5,8 @@
import { FX_watermarkPlacement } from './image-effector/fxs/watermarkPlacement.js';
import { FX_stripe } from './image-effector/fxs/stripe.js';
import { FX_dottedGrid } from './image-effector/fxs/dottedGrid.js';
import { FX_checker } from './image-effector/fxs/checker.js';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
@ -39,6 +41,25 @@ export type WatermarkPreset = {
threshold: number;
black: boolean;
opacity: number;
} | {
id: string;
type: 'dottedGrid';
angle: number;
scale: number;
majorRadius: number;
majorOpacity: number;
minorDivisions: number;
minorRadius: number;
minorOpacity: number;
black: boolean;
opacity: number;
} | {
id: string;
type: 'checker';
angle: number;
scale: number;
black: boolean;
opacity: number;
})[];
};
@ -57,7 +78,7 @@ export class WatermarkRenderer {
renderWidth: options.renderWidth,
renderHeight: options.renderHeight,
image: options.image,
fxs: [FX_watermarkPlacement, FX_stripe],
fxs: [FX_watermarkPlacement, FX_stripe, FX_dottedGrid, FX_checker],
});
}
@ -109,6 +130,33 @@ export class WatermarkRenderer {
opacity: layer.opacity,
},
};
} else if (layer.type === 'dottedGrid') {
return {
fxId: 'dottedGrid',
id: layer.id,
params: {
angle: layer.angle,
scale: layer.scale,
majorRadius: layer.majorRadius,
majorOpacity: layer.majorOpacity,
minorDivisions: layer.minorDivisions,
minorRadius: layer.minorRadius,
minorOpacity: layer.minorOpacity,
black: layer.black,
opacity: layer.opacity,
},
};
} else if (layer.type === 'checker') {
return {
fxId: 'checker',
id: layer.id,
params: {
angle: layer.angle,
scale: layer.scale,
black: layer.black,
opacity: layer.opacity,
},
};
}
});
}