Compare commits
1 Commits
develop
...
frame-imag
| Author | SHA1 | Date |
|---|---|---|
|
|
c9b5e66cdb |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue