feat: add effects
feat: add setting types
This commit is contained in:
parent
6b56e11119
commit
8d1567a54a
3 changed files with 177 additions and 59 deletions
BIN
docs/app.png
BIN
docs/app.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 788 KiB |
|
|
@ -13,8 +13,8 @@ const resultLoading = ref(false)
|
|||
const availableEffects = ref([])
|
||||
const effectToAdd = ref(null)
|
||||
|
||||
const makeSetting = (label, value, min, max, step) => {
|
||||
return {label, value, min, max, step}
|
||||
const makeSetting = (type, label, value, options) => {
|
||||
return {type, label, value, options}
|
||||
}
|
||||
|
||||
const openImageFile = () => {
|
||||
|
|
@ -29,80 +29,127 @@ const openImageFile = () => {
|
|||
|
||||
availableEffects.value = [
|
||||
{
|
||||
label: "Crop",
|
||||
name: "crop",
|
||||
enabled: false,
|
||||
label: "Blur",
|
||||
name: "blur",
|
||||
enabled: true,
|
||||
settings: {
|
||||
x0: makeSetting("Top left X", 0, 0, info.width-1, 1),
|
||||
y0: makeSetting("Top left Y", 0, 0, info.height-1, 1),
|
||||
x1: makeSetting("Bottom right X", 0, 0, info.width, 1),
|
||||
y1: makeSetting("Bottom right left Y", 0, 0, info.height, 1),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Crop center",
|
||||
name: "crop-center",
|
||||
enabled: false,
|
||||
settings: {
|
||||
width: makeSetting("Width", info.width, 0, info.width, 1),
|
||||
height: makeSetting("Height", info.height, 0, info.height, 1)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Scale width",
|
||||
name: "scale-width",
|
||||
enabled: false,
|
||||
settings: {
|
||||
value: makeSetting("Value", info.width, 1, info.width, 1),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Scale height",
|
||||
name: "scale-height",
|
||||
enabled: false,
|
||||
settings: {
|
||||
value: makeSetting("Value", info.height, 1, info.height, 1),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Resize",
|
||||
name: "resize",
|
||||
enabled: false,
|
||||
settings: {
|
||||
width: makeSetting("Width",info.width, 1, info.width, 1),
|
||||
height: makeSetting("Height", info.height, 1, info.height, 1)
|
||||
value: makeSetting("number", "Value", 0, {min: 0, max: 50, step: 0.1}),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Brightness",
|
||||
name: "brightness",
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
settings: {
|
||||
value: makeSetting("Value", 0, -100, 100, 1),
|
||||
value: makeSetting("number", "Value", 0, {min: -100, max: 100, step: 1}),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Crop",
|
||||
name: "crop",
|
||||
enabled: true,
|
||||
settings: {
|
||||
x0: makeSetting("number", "Top left X", 0, {min: 0, max: info.width-1, step: 1}),
|
||||
y0: makeSetting("number", "Top left Y", 0, {min: 0, max: info.height-1, step: 1}),
|
||||
x1: makeSetting("number", "Bottom right X", 0, {min: 1, max: info.width, step: 1}),
|
||||
y1: makeSetting("number", "Bottom right left Y", 0, {min: 1, max: info.height, step: 1}),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Crop center",
|
||||
name: "crop-center",
|
||||
enabled: true,
|
||||
settings: {
|
||||
width: makeSetting("number", "Width", info.width, {min: 0, max: info.width, step: 1}),
|
||||
height: makeSetting("number", "Height", info.height, {min: 0, max: info.height, step: 1}),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Contrast",
|
||||
name: "contrast",
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
settings: {
|
||||
value: makeSetting("Value", 0, -100, 100, 1),
|
||||
value: makeSetting("number", "Value", 0, {min: -100, max: 100, step: 1}),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Flip horizontally",
|
||||
name: "flip-h",
|
||||
enabled: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
label: "Flip vertically",
|
||||
name: "flip-v",
|
||||
enabled: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
label: "Resize",
|
||||
name: "resize",
|
||||
enabled: true,
|
||||
settings: {
|
||||
width: makeSetting("number", "Width", info.width, {min: 1, max: info.width, step: 1}),
|
||||
height: makeSetting("number", "Height", info.height, {min: 1, max: info.height, step: 1}),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Rotate",
|
||||
name: "rotate",
|
||||
enabled: true,
|
||||
settings: {
|
||||
value: makeSetting("number", "Value", 0, {min: 0, max: 360, step: 1}),
|
||||
backgroundColor: makeSetting("color", "Background", "#ffffff", {}),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Rotate 90",
|
||||
name: "rotate-90",
|
||||
enabled: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
label: "Rotate 180",
|
||||
name: "rotate-180",
|
||||
enabled: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
label: "Rotate 270",
|
||||
name: "rotate-270",
|
||||
enabled: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
label: "Saturation",
|
||||
name: "saturation",
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
settings: {
|
||||
value: makeSetting("Value", 0, -100, 100, 1),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Blur",
|
||||
name: "blur",
|
||||
enabled: false,
|
||||
label: "Scale height",
|
||||
name: "scale-height",
|
||||
enabled: true,
|
||||
settings: {
|
||||
value: makeSetting("Value", 0, 0, 50, 0.1),
|
||||
value: makeSetting("Value", info.height, 1, info.height, 1),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Scale width",
|
||||
name: "scale-width",
|
||||
enabled: true,
|
||||
settings: {
|
||||
value: makeSetting("Value", info.width, 1, info.width, 1),
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Quality",
|
||||
name: "quality",
|
||||
enabled: true,
|
||||
settings: {
|
||||
value: makeSetting("Value", 100, 0, 100, 1),
|
||||
}
|
||||
},
|
||||
]
|
||||
|
|
@ -133,7 +180,7 @@ const appliedEffects = () => {
|
|||
}
|
||||
|
||||
for (let setting in effect.settings) {
|
||||
item.settings[setting] = parseFloat(effect.settings[setting].value)
|
||||
item.settings[setting] = effect.settings[setting].value
|
||||
}
|
||||
|
||||
result.push(item)
|
||||
|
|
@ -193,6 +240,12 @@ const deleteEffect = (index) => {
|
|||
effects.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const getAvailableEffects = () => {
|
||||
return availableEffects.value.sort((a, b) => {
|
||||
return a.label > b.label ? 1 : -1
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(openImageFile)
|
||||
</script>
|
||||
|
||||
|
|
@ -224,20 +277,32 @@ onMounted(openImageFile)
|
|||
</div>
|
||||
<div class="effect-settings" v-if="element.enabled">
|
||||
<div v-for="setting, key in element.settings" :key="element.id + key" class="effect-setting">
|
||||
<input
|
||||
type="range"
|
||||
:min="setting.min"
|
||||
:max="setting.max"
|
||||
:step="setting.step"
|
||||
v-model="setting.value"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
:min="setting.min"
|
||||
:max="setting.max"
|
||||
v-model="setting.value"
|
||||
>
|
||||
{{ setting.label }}
|
||||
<div class="effect-setting-label">
|
||||
{{ setting.label }}
|
||||
</div>
|
||||
|
||||
<div v-if="setting.type == 'number'" class="effect-setting-form" :class="['effect-setting-form--' + setting.type]">
|
||||
<input
|
||||
type="range"
|
||||
:min="setting.options.min"
|
||||
:max="setting.options.max"
|
||||
:step="setting.options.step"
|
||||
v-model="setting.value"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
:min="setting.options.min"
|
||||
:max="setting.options.max"
|
||||
:step="setting.options.step"
|
||||
v-model="setting.value"
|
||||
>
|
||||
</div>
|
||||
<div v-if="setting.type == 'color'" class="effect-setting-form" :class="['effect-setting-form--' + setting.type]">
|
||||
<input
|
||||
type="color"
|
||||
v-model="setting.value"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -247,7 +312,7 @@ onMounted(openImageFile)
|
|||
<div>
|
||||
<select v-model="effectToAdd">
|
||||
<option
|
||||
v-for="effect in availableEffects"
|
||||
v-for="effect in getAvailableEffects()"
|
||||
:key="effect.name"
|
||||
:value="effect.name">
|
||||
{{ effect.label }}
|
||||
|
|
@ -257,12 +322,12 @@ onMounted(openImageFile)
|
|||
Add
|
||||
</button>
|
||||
</div>
|
||||
<button @click="applyEffects" :disabled="resultLoading" v-if="effects.length > 0">
|
||||
<button @click="applyEffects" :disabled="resultLoading">
|
||||
<template v-if="resultLoading">
|
||||
Loading...
|
||||
</template>
|
||||
<template v-else>
|
||||
Apply
|
||||
Update result
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -390,6 +455,7 @@ onMounted(openImageFile)
|
|||
display: flex;
|
||||
gap: 10px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
|
@ -398,11 +464,33 @@ onMounted(openImageFile)
|
|||
|
||||
&-setting {
|
||||
padding: 5px 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
input {
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&-label {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
&-form {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&--number {
|
||||
input[type="range"] {
|
||||
width: 70%;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import (
|
|||
"bytes"
|
||||
"encoding/base64"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
_ "image/png"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/spf13/cast"
|
||||
|
|
@ -25,8 +27,8 @@ type ImageInfo struct {
|
|||
}
|
||||
|
||||
type ImageEffect struct {
|
||||
Name string `json:"name"`
|
||||
Settings map[string]float32 `json:"settings"`
|
||||
Name string `json:"name"`
|
||||
Settings map[string]any `json:"settings"`
|
||||
}
|
||||
|
||||
func GetImageInfo(file string) *ImageInfo {
|
||||
|
|
@ -65,6 +67,7 @@ func GetImageInfo(file string) *ImageInfo {
|
|||
func ApplyFilters(src *ImageInfo, effects []ImageEffect) *ImageInfo {
|
||||
base := GetImageInfo(src.Path)
|
||||
result := base.Image
|
||||
quality := 100
|
||||
|
||||
for _, effect := range effects {
|
||||
if effect.Name == "scale-width" {
|
||||
|
|
@ -120,6 +123,33 @@ func ApplyFilters(src *ImageInfo, effects []ImageEffect) *ImageInfo {
|
|||
result = imaging.AdjustGamma(result, cast.ToFloat64(effect.Settings["value"]))
|
||||
} else if effect.Name == "blur" {
|
||||
result = imaging.Blur(result, cast.ToFloat64(effect.Settings["value"]))
|
||||
} else if effect.Name == "flip-v" {
|
||||
result = imaging.FlipV(result)
|
||||
} else if effect.Name == "flip-h" {
|
||||
result = imaging.FlipH(result)
|
||||
} else if effect.Name == "rotate-90" {
|
||||
result = imaging.Rotate90(result)
|
||||
} else if effect.Name == "rotate-180" {
|
||||
result = imaging.Rotate180(result)
|
||||
} else if effect.Name == "rotate-270" {
|
||||
result = imaging.Rotate270(result)
|
||||
} else if effect.Name == "quality" {
|
||||
quality = cast.ToInt(effect.Settings["value"])
|
||||
} else if effect.Name == "rotate" {
|
||||
values, err := strconv.ParseUint(string(effect.Settings["backgroundColor"].(string)[1:]), 16, 32)
|
||||
|
||||
if err == nil {
|
||||
result = imaging.Rotate(
|
||||
result,
|
||||
cast.ToFloat64(effect.Settings["value"]),
|
||||
color.RGBA{
|
||||
R: uint8(values >> 16),
|
||||
G: uint8((values >> 8) & 0xFF),
|
||||
B: uint8(values & 0xFF),
|
||||
A: 255,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +157,7 @@ func ApplyFilters(src *ImageInfo, effects []ImageEffect) *ImageInfo {
|
|||
|
||||
if src.Type == "jpeg" {
|
||||
err := jpeg.Encode(buff, result, &jpeg.Options{
|
||||
Quality: 100,
|
||||
Quality: quality,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue