Compare commits

...

1 Commits

Author SHA1 Message Date
syuilo c9b5e66cdb wip 2025-12-16 16:50:20 +09:00
4 changed files with 57 additions and 4 deletions

View File

@ -31,6 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.controls">
<div class="_spacer _gaps">
<MkButton inline rounded primary @click="chooseFile">{{ i18n.ts.selectFile }}</MkButton>
<MkRange v-model="params.borderThickness" :min="0" :max="0.2" :step="0.01" :continuousUpdate="true">
<template #label>{{ i18n.ts._imageFrameEditor.borderThickness }}</template>
</MkRange>
@ -175,6 +177,7 @@ import { ensureSignin } from '@/i.js';
import { genId } from '@/utility/id.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { prefer } from '@/preferences.js';
import { selectFile } from '@/utility/drive.js';
const $i = ensureSignin();
@ -216,6 +219,7 @@ const params = reactive<ImageFrameParams>(deepClone(props.params) ?? {
bgColor: [1, 1, 1],
fgColor: [0, 0, 0],
font: 'sans-serif',
imageUrl: null,
});
const emit = defineEmits<{
@ -409,6 +413,28 @@ function getRgb(hex: string | number): [number, number, number] | null {
if (m == null) return [0, 0, 0];
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
}
function chooseFile(ev: MouseEvent) {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
label: i18n.ts.selectFile,
features: {
watermark: false,
},
}).then((file) => {
if (!file.type.startsWith('image')) {
os.alert({
type: 'warning',
title: i18n.ts._watermarkEditor.driveFileTypeWarn,
text: i18n.ts._watermarkEditor.driveFileTypeWarnDescription,
});
return;
}
params.imageUrl = file.url;
});
}
</script>
<style module>

View File

@ -30,6 +30,7 @@ export type ImageFrameParams = {
fgColor: [r: number, g: number, b: number];
font: 'serif' | 'sans-serif';
borderRadius: number; // TODO
imageUrl: string | null;
};
export type ImageFramePreset = {
@ -241,6 +242,17 @@ export class ImageFrameRenderer {
this.compositor.registerTexture('bottomLabel', bottomLabelImage);
}
if (params.imageUrl != null) {
const frameImage = new Image();
frameImage.crossOrigin = 'anonymous';
await new Promise<void>((resolve, reject) => {
frameImage.onload = () => resolve();
frameImage.onerror = () => reject(new Error('Failed to load frame image'));
frameImage.src = params.imageUrl!;
});
this.compositor.registerTexture('frameImage', frameImage);
}
this.compositor.changeResolution(renderWidth, renderHeight);
this.compositor.render([{
@ -248,6 +260,7 @@ export class ImageFrameRenderer {
id: 'a',
params: {
image: 'image',
frameImage: 'frameImage',
topLabel: 'topLabel',
bottomLabel: 'bottomLabel',
topLabelEnabled: params.labelTop.enabled,

View File

@ -10,6 +10,7 @@ in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform sampler2D u_image;
uniform sampler2D u_frameImage;
uniform sampler2D u_topLabel;
uniform sampler2D u_bottomLabel;
uniform bool u_topLabelEnabled;
@ -37,6 +38,9 @@ void main() {
remap(in_uv.y, u_paddingTop, 1.0 - u_paddingBottom, 0.0, 1.0)
));
vec4 imageFrame_color = texture(u_frameImage, in_uv);
image_color = vec4(blendAlpha(image_color.rgb, imageFrame_color), 1.0);
vec4 topLabel_color = u_topLabelEnabled ? texture(u_topLabel, vec2(
in_uv.x,
remap(in_uv.y, 0.0, u_paddingTop, 0.0, 1.0)

View File

@ -8,6 +8,7 @@ import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js';
export const FN_frame = defineImageCompositorFunction<{
image: string | null;
frameImage: string | null;
topLabel: string | null;
bottomLabel: string | null;
topLabelEnabled: boolean;
@ -36,21 +37,30 @@ export const FN_frame = defineImageCompositorFunction<{
gl.uniform1f(u.paddingRight, params.paddingRight);
gl.uniform3f(u.bg, params.bg[0], params.bg[1], params.bg[2]);
if (params.frameImage != null) {
const frameImage = textures.get(params.frameImage);
if (frameImage) {
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, frameImage.texture);
gl.uniform1i(u.frameImage, 2);
}
}
if (params.topLabelEnabled && params.topLabel != null) {
const topLabel = textures.get(params.topLabel);
if (topLabel) {
gl.activeTexture(gl.TEXTURE2);
gl.activeTexture(gl.TEXTURE3);
gl.bindTexture(gl.TEXTURE_2D, topLabel.texture);
gl.uniform1i(u.topLabel, 2);
gl.uniform1i(u.topLabel, 3);
}
}
if (params.bottomLabelEnabled && params.bottomLabel != null) {
const bottomLabel = textures.get(params.bottomLabel);
if (bottomLabel) {
gl.activeTexture(gl.TEXTURE3);
gl.activeTexture(gl.TEXTURE4);
gl.bindTexture(gl.TEXTURE_2D, bottomLabel.texture);
gl.uniform1i(u.bottomLabel, 3);
gl.uniform1i(u.bottomLabel, 4);
}
}
},