Skip to content

Instantly share code, notes, and snippets.

@tomhermans
Last active March 21, 2026 18:15
Show Gist options
  • Select an option

  • Save tomhermans/59010e6ace39d08d66732e8ce630f598 to your computer and use it in GitHub Desktop.

Select an option

Save tomhermans/59010e6ace39d08d66732e8ce630f598 to your computer and use it in GitHub Desktop.
Min & Max: The 24-Effects Image Editor
<div class=fullscreen-image><img alt="Image à transformer" crossorigin=anonymous id=meme-image src="https://images.unsplash.com/photo-1637066725847-9eb5c63e1956?w=1600"></div>
<div class=controls-container><button id=togglePanelBtn class=toggle-btn><svg fill=#fff version=1.1 viewBox="0 0 122.88 96.32" x=0px xmlns=http://www.w3.org/2000/svg y=0px>
<g>
<path d="M104.54,23.28c6.82,6.28,12.8,14.02,17.67,22.87l0.67,1.22l-0.67,1.21c-6.88,12.49-15.96,22.77-26.48,29.86 c-8.84,5.95-18.69,9.67-29.15,10.59l6.73-11.66c5.25-1.42,10.24-3.76,14.89-6.9c8.18-5.51,15.29-13.45,20.79-23.1 c-2.98-5.22-6.43-9.94-10.26-14.05L104.54,23.28L104.54,23.28z M88.02,0l17.84,10.3L56.2,96.32l-17.83-10.3l0.69-1.2 c-4.13-1.69-8.11-3.83-11.9-6.38C16.62,71.35,7.55,61.07,0.67,48.59L0,47.37l0.67-1.22C7.55,33.67,16.62,23.39,27.15,16.3 C37.42,9.38,49.08,5.48,61.44,5.48c7.35,0,14.44,1.38,21.14,3.94L88.02,0L88.02,0L88.02,0z M44.36,75.63l5-8.67 c-5.94-3.78-9.89-10.42-9.89-17.99c0-11.77,9.54-21.31,21.31-21.31c3.56,0,6.92,0.87,9.87,2.42l6.61-11.44 c-5.04-1.85-10.35-2.85-15.83-2.85c-9.61,0-18.71,3.06-26.76,8.48c-8.18,5.51-15.29,13.45-20.8,23.11c5.5,9.66,12.62,17.6,20.8,23.1C37.76,72.55,41,74.28,44.36,75.63L44.36,75.63z M63.93,41.74l6.73-11.66 c-1.82-0.95-3.77-1.64-5.79-2.03c-1.45,2.18-2.31,4.82-2.31,7.67C62.56,37.88,63.06,39.93,63.93,41.74L63.93,41.74L63.93,41.74z" />
</g>
</svg></button>
<div class=effects-panel id=effectsPanel>
<div class=fixed-header>
<h2>Controls</h2>
<div class=upload-section>
<div class=button-group><button id=custom-upload-btn class=upload-btn>Upload image</button> <input accept=image/* id=image-upload type=file> <button id=export-btn title="Exporter l'image avec les effets">Export image</button></div>
<div class=file-name id=file-name></div>
</div>
</div>
<div class=scrollable-content>
<div id=effects-list></div>
</div>
<div class=reset-button><button id=reset-all>↺ RÉINITIALISER</button></div>
</div>
</div>
<div class="infos" id="infos">
<p>Photo de <a href="https://unsplash.com/fr/@hamza01nsr?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank" rel="noopener nofollow">Emad Kolahi</a> sur <a href="https://unsplash.com/fr/photos/un-homme-et-une-femme-debout-lun-a-cote-de-lautre-PpMCowfmeto?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank" rel="noopener nofollow">Unsplash</a></p>
</div>
<div class="copy">&Toc</div>
<script>
! function() {
"use strict";
const e = {
fr: {
controlsTitle: "Contrôles",
uploadBtn: "Importer une image",
exportBtn: "Exporter",
resetBtn: "↺ RÉINITIALISER",
fileName: "• ",
fileDefault: "Aucun fichier",
exportLoading: "Export en cours...",
exportPreparing: "Préparation de l'image",
exportCanvas: "Création du canvas...",
exportTransforms: "Application des transformations...",
exportFilters: "Application des filtres...",
exportBorders: "Application des bordures arrondies...",
exportGenerating: "Génération du fichier...",
exportComplete: "Téléchargement terminé !",
exportError: "Erreur: ",
effects: {
blur: "Flou",
brightness: "Luminosité",
contrast: "Contraste",
saturate: "Saturation",
hueRotate: "Teinte",
sepia: "Sépia",
invert: "Négatif",
grayscale: "Niveaux gris",
opacity: "Opacité",
scale: "Zoom*",
rotate: "Rotation",
skewX: "Inclinaison X",
skewY: "Inclinaison Y",
shadowX: "Ombre X",
shadowY: "Ombre Y",
shadowBlur: "Flou ombre",
borderRadius: "Bords arrondis",
neon: "Néon",
rainbow: "Arc-en-ciel*",
vintage: "Masque vintage",
hologram: "Hologramme*",
sunset: "Coucher soleil",
frost: "Givre",
fireworks: "Feu d'artifice"
}
},
en: {
controlsTitle: "Controls",
uploadBtn: "Upload image",
exportBtn: "Export",
resetBtn: "↺ RESET",
fileName: "• ",
fileDefault: "No file",
exportLoading: "Exporting...",
exportPreparing: "Preparing image",
exportCanvas: "Creating canvas...",
exportTransforms: "Applying transformations...",
exportFilters: "Applying filters...",
exportBorders: "Applying rounded corners...",
exportGenerating: "Generating file...",
exportComplete: "Download complete!",
exportError: "Error: ",
effects: {
blur: "Blur",
brightness: "Brightness",
contrast: "Contrast",
saturate: "Saturation",
hueRotate: "Hue",
sepia: "Sepia",
invert: "Invert",
grayscale: "Grayscale",
opacity: "Opacity",
scale: "Zoom*",
rotate: "Rotate",
skewX: "Skew X",
skewY: "Skew Y",
shadowX: "Shadow X",
shadowY: "Shadow Y",
shadowBlur: "Shadow blur",
borderRadius: "Border radius",
neon: "Neon",
rainbow: "Rainbow*",
vintage: "Vintage mask",
hologram: "Hologram*",
sunset: "Sunset",
frost: "Frost",
fireworks: "Fireworks"
}
},
es: {
controlsTitle: "Controles",
uploadBtn: "Subir imagen",
exportBtn: "Exportar",
resetBtn: "↺ REINICIAR",
fileName: "• ",
fileDefault: "Sin archivo",
exportLoading: "Exportando...",
exportPreparing: "Preparando imagen",
exportCanvas: "Creando canvas...",
exportTransforms: "Aplicando transformaciones...",
exportFilters: "Aplicando filtros...",
exportBorders: "Aplicando bordes redondeados...",
exportGenerating: "Generando archivo...",
exportComplete: "¡Descarga completa!",
exportError: "Error: ",
effects: {
blur: "Desenfoque",
brightness: "Brillo",
contrast: "Contraste",
saturate: "Saturación",
hueRotate: "Tono",
sepia: "Sepia",
invert: "Negativo",
grayscale: "Escala grises",
opacity: "Opacidad",
scale: "Zoom*",
rotate: "Rotar",
skewX: "Inclinación X",
skewY: "Inclinación Y",
shadowX: "Sombra X",
shadowY: "Sombra Y",
shadowBlur: "Desenfoque sombra",
borderRadius: "Bordes redondeados",
neon: "Neón",
rainbow: "Arcoíris*",
vintage: "Máscara vintage",
hologram: "Holograma*",
sunset: "Atardecer",
frost: "Escarcha",
fireworks: "Fuegos artificiales"
}
}
},
t = "\n .language-btn {\n position: fixed;\n bottom: 24px;\n left: 28px;\n z-index: 2000;\n background: rgba(30, 30, 46, 0.95);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n border: 1px solid rgba(255, 255, 255, 0.18);\n color: white;\n padding: 12px 20px;\n border-radius: 48px;\n font-size: 1rem;\n font-weight: 600;\n letter-spacing: 1px;\n display: flex;\n align-items: center;\n gap: 8px;\n box-shadow: 0 10px 30px rgba(0,0,0,0.5);\n transition: 0.2s;\n cursor: pointer;\n border: none;\n }\n .language-btn:hover {\n background: rgba(50, 50, 70, 0.95);\n border-color: tomato;\n transform: scale(1.02);\n box-shadow: 0 14px 35px rgba(0,0,0,0.6);\n }\n .language-menu {\n position: fixed;\n bottom: 88px;\n left: 28px;\n z-index: 1999;\n background: rgba(30, 30, 46, 0.95);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n border: 1px solid rgba(255,255,255,0.15);\n border-radius: 24px;\n padding: 8px 0;\n display: none;\n flex-direction: column;\n min-width: 180px;\n box-shadow: 0 20px 40px rgba(0,0,0,0.6);\n }\n .language-menu.show {\n display: flex;\n }\n .language-option {\n background: transparent;\n border: none;\n color: #eef3f5;\n padding: 12px 24px;\n text-align: left;\n font-size: 1rem;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 12px;\n transition: 0.1s;\n cursor: pointer;\n border-left: 4px solid transparent;\n }\n .language-option:hover {\n background: rgba(255,255,255,0.08);\n color: white;\n }\n .language-option.active {\n background: rgba(255, 99, 71, 0.2);\n }\n .language-option span {\n font-size: 1.2rem;\n }\n ";
let n = "en";
function o() {
const t = e[n],
o = document.querySelector(".effects-panel h2");
o && (o.textContent = t.controlsTitle);
const a = document.getElementById("custom-upload-btn");
a && (a.innerHTML = `${t.uploadBtn}`);
const i = document.getElementById("export-btn");
i && !i.disabled && (i.innerHTML = `${t.exportBtn}`);
const s = document.getElementById("reset-all");
s && (s.textContent = t.resetBtn);
const l = document.getElementById("file-name");
if (l && !l.textContent.includes(t.fileName)) {
const e = l.textContent;
e && !e.includes("📸") ? l.textContent = e : e || (l.textContent = "")
}! function() {
const t = document.querySelectorAll(".effect-item"),
o = e[n];
t.forEach((e => {
const t = e.querySelector(".effect-header label"),
n = e.querySelector('.effect-header input[type="checkbox"]');
if (t && n) {
const e = {
blur: "blur",
brightness: "brightness",
contrast: "contrast",
saturate: "saturate",
"hue-rotate": "hueRotate",
sepia: "sepia",
invert: "invert",
grayscale: "grayscale",
opacity: "opacity",
scale: "scale",
rotate: "rotate",
skewX: "skewX",
skewY: "skewY",
shadowX: "shadowX",
shadowY: "shadowY",
shadowBlur: "shadowBlur",
borderRadius: "borderRadius",
neon: "neon",
rainbow: "rainbow",
vintage: "vintage",
hologram: "hologram",
sunset: "sunset",
frost: "frost",
fireworks: "fireworks"
} [n.id.replace("check-", "")];
e && o.effects[e] && (t.textContent = o.effects[e])
}
}))
}(), r()
}
function r() {
const t = document.getElementById("export-loading-overlay");
if (!t) return;
const o = e[n],
r = t.querySelector("div:nth-child(2)"),
a = document.getElementById("export-status");
if (r && (r.textContent = o.exportLoading), a) {
const e = a.textContent;
e.includes("Préparation") ? a.textContent = o.exportPreparing : e.includes("Création") ? a.textContent = o.exportCanvas : e.includes("Application des transformations") ? a.textContent = o.exportTransforms : e.includes("Application des filtres") ? a.textContent = o.exportFilters : e.includes("Application des bordures") ? a.textContent = o.exportBorders : e.includes("Génération") ? a.textContent = o.exportGenerating : e.includes("Téléchargement terminé") ? a.textContent = o.exportComplete : e.includes("Erreur") && (a.textContent = o.exportError + e.split(":")[1])
}
}
function a() {
const e = document.createElement("style");
e.textContent = t, document.head.appendChild(e);
const r = document.createElement("button");
r.id = "languageBtn", r.className = "language-btn", r.innerHTML = `🌐 ${n.toUpperCase()}`;
const a = document.createElement("div");
a.id = "languageMenu", a.className = "language-menu";
[{
code: "fr",
name: "Français",
flag: "🇫🇷"
}, {
code: "en",
name: "English",
flag: "🇬🇧"
}, {
code: "es",
name: "Español",
flag: "🇪🇸"
}].forEach((e => {
const t = document.createElement("button");
t.className = "language-option", e.code === n && t.classList.add("active"), t.dataset.lang = e.code, t.innerHTML = `<span>${e.flag}</span> ${e.name}`, t.addEventListener("click", (r => {
r.stopPropagation(),
function(e) {
if (n === e) return;
n = e;
const t = document.getElementById("languageBtn");
t && (t.innerHTML = `🌐 ${e.toUpperCase()}`);
o(), window.dispatchEvent(new CustomEvent("languageChanged", {
detail: {
language: e
}
}))
}(e.code), document.querySelectorAll(".language-option").forEach((e => {
e.classList.remove("active")
})), t.classList.add("active"), a.classList.remove("show")
})), a.appendChild(t)
})), document.body.appendChild(r), document.body.appendChild(a), r.addEventListener("click", (e => {
e.stopPropagation(), a.classList.toggle("show")
})), document.addEventListener("click", (e => {
r.contains(e.target) || a.contains(e.target) || a.classList.remove("show")
}))
}
a(),
function() {
if (window.createLoadingOverlay, window.createLoadingOverlay = function() {
const t = document.createElement("div");
t.id = "export-loading-overlay";
const o = document.createElement("div");
o.className = "export-spinner";
const r = document.createElement("div");
r.textContent = e[n].exportLoading, r.style.fontSize = "18px", r.style.fontWeight = "500", r.style.marginBottom = "10px";
const a = document.createElement("div");
return a.id = "export-status", a.textContent = e[n].exportPreparing, a.style.fontSize = "14px", a.style.opacity = "0.8", t.appendChild(o), t.appendChild(r), t.appendChild(a), t
}, window.exportImage) {
const t = window.exportImage;
window.exportImage = async function() {
const o = document.getElementById("export-btn");
o.innerHTML, o.innerHTML = `${e[n].exportBtn}...`;
try {
await t()
} finally {
o.innerHTML = `${e[n].exportBtn}`
}
}
}
}(), "loading" === document.readyState ? document.addEventListener("DOMContentLoaded", (() => {
setTimeout(o, 100)
})) : setTimeout(o, 100), new MutationObserver((e => {
e.forEach((e => {
"childList" === e.type && e.addedNodes.length > 0 && e.addedNodes.forEach((e => {
("export-loading-overlay" === e.id || 1 === e.nodeType && e.querySelector && e.querySelector("#export-loading-overlay")) && r()
}))
}))
})).observe(document.body, {
childList: !0,
subtree: !0
})
}();
</script>

Min & Max: The 24-Effects Image Editor

Transform your photos with 24 amazing visual effects: blur, brightness, saturation, neon, hologram and more. Each slider uses Math.min() and Math.max() methods to ensure values always stay within defined boundaries.

Features : • 24 customizable effects (CSS filters, transformations, shadows) • Retractable floating interface • Drag & drop image import • High-quality PNG export • 3 languages available (EN/FR/ES)

Note : Effects marked with an asterisk () are not exportable*.

A Pen by Toc on CodePen.

License.

function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
function hexToRgb(hex) {
hex = hex.replace(/^#/, "");
if (hex.length === 3)
hex = hex
.split("")
.map((c) => c + c)
.join("");
const num = parseInt(hex, 16);
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255
};
}
function rgbToHsv(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h,
s,
v = max;
const d = max - min;
s = max === 0 ? 0 : d / max;
if (max === min) h = 0;
else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
v: Math.round(v * 100)
};
}
function hsvToRgb(h, s, v) {
h /= 360;
s /= 100;
v /= 100;
let r, g, b;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function loadImageWithCORS(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = () => {
const img2 = new Image();
img2.onload = () => resolve(img2);
img2.onerror = reject;
img2.src = url;
};
img.src = url;
console.log("&Toc on codepen - https://codepen.io/ol-ivier");
});
}
const image = document.getElementById("meme-image");
const imageUpload = document.getElementById("image-upload");
const customUploadBtn = document.getElementById("custom-upload-btn");
const fileName = document.getElementById("file-name");
const effectsList = document.getElementById("effects-list");
const effectsPanel = document.getElementById("effectsPanel");
const toggleBtn = document.getElementById("togglePanelBtn");
const exportBtn = document.getElementById("export-btn");
console.log("&Toc on codepen - https://codepen.io/ol-ivier");
customUploadBtn.addEventListener("click", () => {
imageUpload.click();
});
imageUpload.addEventListener("change", function (e) {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
fileName.textContent = `${file.name}`;
if (file.type.match("image.*")) {
const reader = new FileReader();
reader.onload = (event) => {
image.src = event.target.result;
image.crossOrigin = "anonymous";
};
reader.readAsDataURL(file);
document.querySelector("#infos").style.display = "none";
}
}
});
toggleBtn.addEventListener("click", () => {
effectsPanel.classList.toggle("hidden");
toggleBtn.classList.toggle("rotated");
});
const effects = [
{
name: "Flou",
property: "blur",
unit: "px",
min: 0,
max: 10,
default: 0,
step: 0.1,
type: "filter"
},
{
name: "Luminosité",
property: "brightness",
unit: "%",
min: 20,
max: 200,
default: 100,
step: 1,
type: "filter"
},
{
name: "Contraste",
property: "contrast",
unit: "%",
min: 12,
max: 300,
default: 100,
step: 1,
type: "filter"
},
{
name: "Saturation",
property: "saturate",
unit: "%",
min: 0,
max: 600,
default: 100,
step: 1,
type: "filter"
},
{
name: "Teinte",
property: "hue-rotate",
unit: "deg",
min: 0,
max: 360,
default: 0,
step: 1,
type: "filter"
},
{
name: "Sépia",
property: "sepia",
unit: "%",
min: 0,
max: 100,
default: 0,
step: 1,
type: "filter"
},
{
name: "Négatif",
property: "invert",
unit: "%",
min: 0,
max: 100,
default: 0,
step: 1,
type: "filter"
},
{
name: "Niveaux gris",
property: "grayscale",
unit: "%",
min: 0,
max: 100,
default: 0,
step: 1,
type: "filter"
},
{
name: "Opacité",
property: "opacity",
unit: "%",
min: 10,
max: 100,
default: 100,
step: 1,
type: "filter"
},
{
name: "Zoom*",
property: "scale",
unit: "%",
min: 50,
max: 200,
default: 100,
step: 1,
type: "transform"
},
{
name: "Rotation",
property: "rotate",
unit: "deg",
min: 0,
max: 360,
default: 0,
step: 1,
type: "transform"
},
{
name: "Inclinaison X",
property: "skewX",
unit: "deg",
min: -45,
max: 45,
default: 0,
step: 1,
type: "transform"
},
{
name: "Inclinaison Y",
property: "skewY",
unit: "deg",
min: -45,
max: 45,
default: 0,
step: 1,
type: "transform"
},
{
name: "Ombre X",
property: "shadowX",
unit: "px",
min: -20,
max: 20,
default: 0,
step: 1,
type: "shadow"
},
{
name: "Ombre Y",
property: "shadowY",
unit: "px",
min: -20,
max: 20,
default: 0,
step: 1,
type: "shadow"
},
{
name: "Flou ombre",
property: "shadowBlur",
unit: "px",
min: 0,
max: 200,
default: 0,
step: 1,
type: "shadow"
},
{
name: "Bords arrondis",
property: "borderRadius",
unit: "px",
min: 0,
max: 100,
default: 0,
step: 1,
type: "border"
},
{
name: "Néon",
property: "neon",
unit: "",
min: 0,
max: 100,
default: 0,
step: 1,
type: "neon"
},
{
name: "Arc-en-ciel*",
property: "rainbow",
unit: "",
min: 0,
max: 100,
default: 0,
step: 1,
type: "rainbow"
},
{
name: "Masque vintage",
property: "vintage",
unit: "",
min: 0,
max: 100,
default: 0,
step: 1,
type: "vintage"
},
{
name: "Hologramme*",
property: "hologram",
unit: "",
min: 0,
max: 100,
default: 0,
step: 1,
type: "hologram"
},
{
name: "Coucher soleil",
property: "sunset",
unit: "",
min: 0,
max: 100,
default: 0,
step: 1,
type: "sunset"
},
{
name: "Givre",
property: "frost",
unit: "",
min: 0,
max: 100,
default: 0,
step: 1,
type: "frost"
},
{
name: "Feu d'artifice",
property: "fireworks",
unit: "",
min: 0,
max: 100,
default: 0,
step: 1,
type: "fireworks"
}
];
let enabledEffects = {};
let shadowValues = {
x: 0,
y: 0,
blur: 0
};
effects.forEach((effect) => {
enabledEffects[effect.property] = !0;
});
effects.forEach((effect) => {
const effectDiv = document.createElement("div");
effectDiv.className = "effect-item";
effectDiv.innerHTML = `
<div class="effect-header">
<input type="checkbox" id="check-${effect.property}" checked>
<label for="check-${effect.property}">${effect.name}</label>
<span class="value-display" id="display-${effect.property}">${effect.default}${effect.unit}</span>
</div>
<div class="slider-container">
<input type="range" id="${effect.property}"
min="${effect.min}" max="${effect.max}"
value="${effect.default}" step="${effect.step}">
</div>
`;
effectsList.appendChild(effectDiv);
});
function updateDisplay(effect) {
const slider = document.getElementById(effect.property);
const display = document.getElementById(`display-${effect.property}`);
if (slider && display) {
const value = parseFloat(slider.value);
display.textContent = value + effect.unit;
}
}
function applyEffects() {
effects.forEach((effect) => updateDisplay(effect));
const shadowX = enabledEffects.shadowX
? parseFloat(document.getElementById("shadowX")?.value || 0)
: 0;
const shadowY = enabledEffects.shadowY
? parseFloat(document.getElementById("shadowY")?.value || 0)
: 0;
const shadowBlur = enabledEffects.shadowBlur
? parseFloat(document.getElementById("shadowBlur")?.value || 0)
: 0;
shadowValues = {
x: shadowX,
y: shadowY,
blur: shadowBlur
};
let filterString = "";
let transformString = "";
let borderRadiusString = "";
effects.forEach((effect) => {
const slider = document.getElementById(effect.property);
if (slider && enabledEffects[effect.property]) {
const value = parseFloat(slider.value);
switch (effect.type) {
case "filter":
if (effect.property === "opacity") {
filterString += ` opacity(${value}%)`;
} else if (effect.property === "hue-rotate") {
filterString += ` hue-rotate(${value}deg)`;
} else if (value !== effect.default) {
filterString += ` ${effect.property}(${value}${effect.unit})`;
}
break;
case "transform":
if (effect.property === "scale") {
transformString += ` scale(${value / 100})`;
} else if (value !== effect.default) {
transformString += ` ${effect.property}(${value}${effect.unit})`;
}
break;
case "border":
if (value !== effect.default) {
borderRadiusString = `${value}px`;
}
break;
case "neon":
if (value > 0) {
const neonIntensity = 1 + value / 50;
filterString += ` brightness(${neonIntensity}) saturate(${neonIntensity})`;
}
break;
case "rainbow":
if (value > 0) {
const hue = ((Date.now() * value) / 1000) % 360;
filterString += ` hue-rotate(${hue}deg) saturate(2)`;
}
break;
case "vintage":
if (value > 0) {
const v = value / 100;
filterString += ` sepia(${0.3 + v * 0.7}) contrast(${
1 + v * 0.5
}) brightness(${0.9 + v * 0.2})`;
if (value > 50) filterString += ` blur(0.5px)`;
}
break;
case "hologram":
if (value > 0) {
const h = value / 100;
const hueShift = (Date.now() * 0.1) % 360;
filterString += ` hue-rotate(${hueShift}deg) saturate(${
1 + h * 2
}) brightness(${1 + h * 0.5})`;
}
break;
case "sunset":
if (value > 0) {
const s = value / 100;
filterString += ` sepia(${0.3 + s * 0.5}) hue-rotate(${
-10 + s * 20
}deg) saturate(${1.2 + s}) brightness(${1 + s * 0.3})`;
}
break;
case "frost":
if (value > 0) {
const f = value / 100;
filterString += ` grayscale(${
0.2 + f * 0.5
}) brightness(${1.2}) contrast(${1 - f * 0.3}) blur(${
f * 2
}px) sepia(0.2) hue-rotate(180deg)`;
}
break;
case "fireworks":
if (value > 0) {
filterString += ` brightness(${1.5}) saturate(${3}) contrast(1.2)`;
}
break;
}
}
});
const hologramActive =
enabledEffects.hologram &&
parseFloat(document.getElementById("hologram")?.value || 0) > 0;
const fireworksActive =
enabledEffects.fireworks &&
parseFloat(document.getElementById("fireworks")?.value || 0) > 0;
if (!hologramActive && !fireworksActive) {
if (shadowValues.x !== 0 || shadowValues.y !== 0 || shadowValues.blur !== 0) {
image.style.boxShadow = `${shadowValues.x}px ${shadowValues.y}px ${shadowValues.blur}px rgba(0,0,0,0.5)`;
} else {
image.style.boxShadow = "none";
}
} else {
image.style.boxShadow = "none";
}
image.style.filter = filterString || "none";
image.style.transform = transformString || "none";
image.style.borderRadius = borderRadiusString || "0";
}
effects.forEach((effect) => {
const slider = document.getElementById(effect.property);
if (slider) {
slider.addEventListener("input", () => {
updateDisplay(effect);
applyEffects();
});
}
const checkbox = document.getElementById(`check-${effect.property}`);
if (checkbox) {
checkbox.addEventListener("change", function () {
enabledEffects[effect.property] = this.checked;
const slider = document.getElementById(effect.property);
if (slider) slider.disabled = !this.checked;
applyEffects();
});
}
});
document.getElementById("reset-all").addEventListener("click", function () {
effects.forEach((effect) => {
const slider = document.getElementById(effect.property);
const checkbox = document.getElementById(`check-${effect.property}`);
if (slider && checkbox) {
slider.value = effect.default;
checkbox.checked = !0;
enabledEffects[effect.property] = !0;
slider.disabled = !1;
updateDisplay(effect);
}
});
image.style.boxShadow = "none";
applyEffects();
fileName.textContent = "";
});
effects.forEach((effect) => updateDisplay(effect));
applyEffects();
function animateEffects() {
if (
enabledEffects.rainbow &&
parseFloat(document.getElementById("rainbow")?.value || 0) > 0
) {
applyEffects();
}
if (
enabledEffects.hologram &&
parseFloat(document.getElementById("hologram")?.value || 0) > 0
) {
applyEffects();
}
if (
enabledEffects.fireworks &&
parseFloat(document.getElementById("fireworks")?.value || 0) > 0
) {
applyEffects();
}
requestAnimationFrame(animateEffects);
}
animateEffects();
function createLoadingOverlay() {
const overlay = document.createElement("div");
overlay.id = "export-loading-overlay";
const spinner = document.createElement("div");
spinner.className = "export-spinner";
const message = document.createElement("div");
message.textContent = "Export en cours...";
message.style.fontSize = "18px";
message.style.fontWeight = "500";
message.style.marginBottom = "10px";
const subMessage = document.createElement("div");
subMessage.id = "export-status";
subMessage.textContent = "Préparation de l'image";
subMessage.style.fontSize = "14px";
subMessage.style.opacity = "0.8";
overlay.appendChild(spinner);
overlay.appendChild(message);
overlay.appendChild(subMessage);
return overlay;
}
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.quadraticCurveTo(x + w, y, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r);
this.lineTo(x, y + r);
this.quadraticCurveTo(x, y, x + r, y);
return this;
};
}
async function exportImage() {
exportBtn.disabled = !0;
exportBtn.style.opacity = "0.7";
exportBtn.innerHTML = "Export...";
const loadingOverlay = createLoadingOverlay();
document.body.appendChild(loadingOverlay);
try {
if (!image.complete || image.naturalWidth === 0) {
throw new Error("L'image n'est pas complètement chargée");
}
const statusEl = document.getElementById("export-status");
const imgWidth = image.naturalWidth;
const imgHeight = image.naturalHeight;
const scale = enabledEffects.scale
? parseFloat(document.getElementById("scale")?.value || 100) / 100
: 1;
const rotate = enabledEffects.rotate
? parseFloat(document.getElementById("rotate")?.value || 0)
: 0;
const skewX = enabledEffects.skewX
? parseFloat(document.getElementById("skewX")?.value || 0)
: 0;
const skewY = enabledEffects.skewY
? parseFloat(document.getElementById("skewY")?.value || 0)
: 0;
const shadowX = enabledEffects.shadowX
? parseFloat(document.getElementById("shadowX")?.value || 0)
: 0;
const shadowY = enabledEffects.shadowY
? parseFloat(document.getElementById("shadowY")?.value || 0)
: 0;
const shadowBlur = enabledEffects.shadowBlur
? parseFloat(document.getElementById("shadowBlur")?.value || 0)
: 0;
const borderRadius = enabledEffects.borderRadius
? parseFloat(document.getElementById("borderRadius")?.value || 0)
: 0;
const angleRad = (rotate * Math.PI) / 180;
const skewXRad = (skewX * Math.PI) / 180;
const skewYRad = (skewY * Math.PI) / 180;
const scaledWidth = imgWidth * scale;
const scaledHeight = imgHeight * scale;
const points = [
{
x: 0,
y: 0
},
{
x: scaledWidth,
y: 0
},
{
x: scaledWidth,
y: scaledHeight
},
{
x: 0,
y: scaledHeight
}
].map((p) => ({
x: p.x + p.y * Math.tan(skewXRad),
y: p.y + p.x * Math.tan(skewYRad)
}));
const centerX = (points[0].x + points[2].x) / 2;
const centerY = (points[0].y + points[2].y) / 2;
const rotatedPoints = points.map((p) => {
const dx = p.x - centerX;
const dy = p.y - centerY;
return {
x: centerX + dx * Math.cos(angleRad) - dy * Math.sin(angleRad),
y: centerY + dx * Math.sin(angleRad) + dy * Math.cos(angleRad)
};
});
const minX = Math.min(...rotatedPoints.map((p) => p.x));
const maxX = Math.max(...rotatedPoints.map((p) => p.x));
const minY = Math.min(...rotatedPoints.map((p) => p.y));
const maxY = Math.max(...rotatedPoints.map((p) => p.y));
const shadowOffset = Math.abs(shadowX) + shadowBlur;
const canvasWidth = Math.ceil(maxX - minX + shadowOffset * 2);
const canvasHeight = Math.ceil(maxY - minY + shadowOffset * 2);
const offsetX = -minX + shadowOffset + (shadowX < 0 ? -shadowX : 0);
const offsetY = -minY + shadowOffset + (shadowY < 0 ? -shadowY : 0);
statusEl.textContent = "Création du canvas...";
const canvas = document.createElement("canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext("2d");
if (shadowBlur > 0 || shadowX !== 0 || shadowY !== 0) {
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowOffsetX = shadowX;
ctx.shadowOffsetY = shadowY;
ctx.shadowBlur = shadowBlur;
}
statusEl.textContent = "Application des transformations...";
ctx.translate(offsetX, offsetY);
ctx.translate(centerX, centerY);
ctx.rotate(angleRad);
ctx.transform(1, Math.tan(skewYRad), Math.tan(skewXRad), 1, 0, 0);
ctx.scale(scale, scale);
ctx.translate(-imgWidth / 2, -imgHeight / 2);
ctx.drawImage(image, 0, 0, imgWidth, imgHeight);
ctx.setTransform(1, 0, 0, 1, 0, 0);
statusEl.textContent = "Application des filtres...";
const filters = [];
effects.forEach((effect) => {
const slider = document.getElementById(effect.property);
if (slider && enabledEffects[effect.property]) {
const value = parseFloat(slider.value);
if (value !== effect.default) {
switch (effect.type) {
case "filter":
if (effect.property === "opacity") {
filters.push(`opacity(${value / 100})`);
} else if (effect.property === "hue-rotate") {
filters.push(`hue-rotate(${value}deg)`);
} else {
filters.push(`${effect.property}(${value}${effect.unit})`);
}
break;
case "neon":
if (value > 0) {
const neonIntensity = 1 + value / 50;
filters.push(`brightness(${neonIntensity})`);
filters.push(`saturate(${neonIntensity})`);
}
break;
case "vintage":
if (value > 0) {
const v = value / 100;
filters.push(`sepia(${0.3 + v * 0.7})`);
filters.push(`contrast(${1 + v * 0.5})`);
filters.push(`brightness(${0.9 + v * 0.2})`);
if (value > 50) filters.push(`blur(0.5px)`);
}
break;
case "sunset":
if (value > 0) {
const s = value / 100;
filters.push(`sepia(${0.3 + s * 0.5})`);
filters.push(`hue-rotate(${-10 + s * 20}deg)`);
filters.push(`saturate(${1.2 + s})`);
filters.push(`brightness(${1 + s * 0.3})`);
}
break;
case "frost":
if (value > 0) {
const f = value / 100;
filters.push(`grayscale(${0.2 + f * 0.5})`);
filters.push(`brightness(1.2)`);
filters.push(`contrast(${1 - f * 0.3})`);
filters.push(`blur(${f * 2}px)`);
filters.push(`sepia(0.2)`);
filters.push(`hue-rotate(180deg)`);
}
break;
case "fireworks":
if (value > 0) {
filters.push(`brightness(1.5)`);
filters.push(`saturate(3)`);
filters.push(`contrast(1.2)`);
}
break;
}
}
}
});
if (filters.length > 0) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const filterCanvas = document.createElement("canvas");
filterCanvas.width = canvas.width;
filterCanvas.height = canvas.height;
const filterCtx = filterCanvas.getContext("2d");
filterCtx.filter = filters.join(" ");
filterCtx.drawImage(canvas, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(filterCanvas, 0, 0);
}
if (borderRadius > 0) {
statusEl.textContent = "Application des bordures arrondies...";
const roundedCanvas = document.createElement("canvas");
roundedCanvas.width = canvas.width;
roundedCanvas.height = canvas.height;
const roundedCtx = roundedCanvas.getContext("2d");
roundedCtx.beginPath();
roundedCtx.roundRect(0, 0, canvas.width, canvas.height, borderRadius);
roundedCtx.closePath();
roundedCtx.clip();
roundedCtx.drawImage(canvas, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(roundedCanvas, 0, 0);
}
statusEl.textContent = "Génération du fichier...";
canvas.toBlob(
(blob) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `image-effets-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
statusEl.textContent = "Téléchargement terminé !";
setTimeout(() => {
document.body.removeChild(loadingOverlay);
}, 1500);
},
"image/png",
1.0
);
} catch (err) {
console.error("Erreur export:", err);
const statusEl = document.getElementById("export-status");
if (statusEl) {
statusEl.textContent = "Erreur: " + err.message;
statusEl.style.color = "#ff4444";
}
setTimeout(() => {
if (loadingOverlay.parentNode) {
document.body.removeChild(loadingOverlay);
}
}, 3000);
} finally {
exportBtn.disabled = !1;
exportBtn.style.opacity = "1";
exportBtn.innerHTML = "Export";
}
}
exportBtn.addEventListener("click", exportImage);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a2e;
position: relative;
}
.copy {
position: fixed;
top: 20px;
left: 20px;
color: #fff;
background: rgb(0 0 0 / 0.9);
padding: 10px;
border-radius: 5px;
font-size: 12px;
pointer-events: none;
z-index: 30;
}
.fullscreen-image {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1;
background: #fbf300;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
}
#meme-image {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
transition: all 0.1s ease;
}
.controls-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
z-index: 1000;
}
.toggle-btn {
width: 40px;
height: 40px;
background: tomato;
border: 2px solid #fff;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 5px 15px rgb(108 92 231 / 0.5);
transition: all 0.3s ease;
margin-right: 10px;
}
.toggle-btn:hover {
transform: scale(1.1);
background: #000;
}
.toggle-btn svg {
width: 30px;
height: 30px;
transition: transform 0.3s ease;
}
.toggle-btn.rotated svg {
transform: rotate(180deg);
}
.effects-panel {
width: 400px;
max-height: 80vh;
background: rgb(30 30 46 / 0.95);
backdrop-filter: blur(10px);
padding: 20px;
padding-top: 0;
box-shadow: 0 20px 50px rgb(0 0 0 / 0.5);
border: 1px solid #4a4a6a;
overflow-y: auto;
transition: opacity 0.3s ease, visibility 0.3s ease;
display: flex;
flex-direction: column;
border-radius: 2em;
}
.effects-panel .fixed-header {
position: sticky;
top: 0;
background: rgb(30 30 46 / 0.95);
backdrop-filter: blur(10px);
z-index: 10;
padding: 20px 20px 10px 20px;
margin: 0 -20px 0 -20px;
border-bottom: 2px solid tomato;
}
.effects-panel .fixed-header h2 {
color: #fff;
margin-bottom: 15px;
text-align: center;
font-size: 1.3em;
border-bottom: none;
padding-bottom: 0;
}
.effects-panel .fixed-header .upload-section {
padding: 12px;
border-radius: 10px;
}
.effects-panel .scrollable-content {
overflow-y: auto;
padding: 10px 0 20px 0;
flex: 1;
margin-top: 10px;
}
.effects-panel .reset-button {
position: sticky;
bottom: 0;
background: rgb(30 30 46 / 0.95);
backdrop-filter: blur(10px);
padding: 10px 0;
margin-top: 10px;
border-top: 1px solid #4a4a6a;
}
.effects-panel.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.effects-panel h2 {
color: #fff;
margin-bottom: 15px;
text-align: center;
font-size: 1.3em;
border-bottom: 2px solid tomato;
padding-bottom: 8px;
}
.upload-section {
/*! margin-bottom: 15px; */
padding: 12px;
/*! background: rgba(108, 92, 231, 0.15); */
border-radius: 10px;
/*! border: 2px dashed tomato; */
display: flex;
flex-direction: column;
gap: 10px;
}
.upload-section label {
display: block;
color: #fff;
margin-bottom: 6px;
font-weight: 700;
font-size: 0.9em;
}
.button-group {
display: flex;
gap: 10px;
}
.upload-btn {
background: #2a2a40;
color: #fff;
border: 2px solid tomato;
padding: 8px 15px;
border-radius: 6px;
font-size: 0.9em;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
display: flex;
align-items: center;
gap: 5px;
flex: 1;
justify-content: center;
}
.upload-btn:hover {
background: tomato;
color: #fff;
border-color: #fff;
}
.upload-btn:active {
transform: scale(0.95);
}
#image-upload {
display: none;
}
#export-btn {
background: tomato;
color: #fff;
border: none;
padding: 8px 15px;
border-radius: 6px;
font-size: 0.9em;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid #fff0;
white-space: nowrap;
display: flex;
align-items: center;
gap: 5px;
}
#export-btn:hover {
background: #fff;
color: tomato;
border-color: tomato;
}
#export-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.effect-item {
background: rgb(50 50 70 / 0.5);
border-radius: 6px;
padding: 6px 8px;
margin-bottom: 4px;
border: 1px solid #4a4a6a;
transition: all 0.2s ease;
}
.effect-item:hover {
border-color: tomato;
background: rgb(60 60 80 / 0.5);
}
.effect-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 2px;
}
.effect-header input[type="checkbox"] {
width: 12px;
height: 12px;
cursor: pointer;
accent-color: tomato;
}
.effect-header label {
color: #fff;
font-weight: 500;
font-size: 0.85em;
cursor: pointer;
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.value-display {
color: #fff;
font-weight: 700;
font-size: 0.8em;
background: rgb(0 0 0 / 0.3);
padding: 1px 6px;
border-radius: 20px;
min-width: 45px;
text-align: center;
border: 1px solid tomato;
}
.slider-container {
margin: 2px 0 0 0;
}
input[type="range"] {
width: 100%;
height: 3px;
-webkit-appearance: none;
appearance: none;
background: #fff0;
}
input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 3px;
background: #fff;
border-radius: 10px;
border: 1px solid rgb(255 255 255 / 0.1);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: tomato;
margin-top: -6px;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgb(108 92 231 / 0.5);
}
input[type="range"]::-moz-range-track {
width: 100%;
height: 3px;
background: #fff;
border-radius: 10px;
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: tomato;
border: 2px solid #fff;
cursor: pointer;
}
input[type="range"]:disabled {
opacity: 0.4;
}
.reset-button {
margin-top: 15px;
text-align: center;
position: sticky;
bottom: 0;
background: rgb(30 30 46 / 0.95);
padding: 10px 0;
backdrop-filter: blur(10px);
}
#reset-all {
background: tomato;
color: #fff;
border: none;
padding: 8px 20px;
border-radius: 50px;
font-size: 0.9em;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid #fff0;
width: 100%;
}
#reset-all:hover {
background: #fff;
color: tomato;
border-color: tomato;
}
.effects-panel::-webkit-scrollbar {
width: 4px;
}
.effects-panel::-webkit-scrollbar-track {
background: #2a2a40;
}
.effects-panel::-webkit-scrollbar-thumb {
background: tomato;
border-radius: 4px;
}
#export-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgb(0 0 0 / 0.9);
backdrop-filter: blur(5px);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
font-family: "Segoe UI", sans-serif;
transition: opacity 0.3s ease;
}
.export-spinner {
width: 50px;
height: 50px;
border: 4px solid rgb(255 255 255 / 0.3);
border-top: 4px solid tomato;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.file-name {
color: #aaa;
font-size: 0.8em;
margin-top: 5px;
text-align: center;
word-break: break-all;
max-width: 100%;
}
@media (max-width: 768px) {
.controls-container {
right: 20px;
}
.effects-panel {
width: 90%;
max-width: 380px;
}
.toggle-btn {
width: 40px;
height: 40px;
}
.toggle-btn svg {
width: 24px;
height: 24px;
}
}
.infos {
position: fixed;
bottom: 20px;
right: 20px;
color: tomato;
background: rgb(255 255 255 / 0.8);
padding: 10px;
border-radius: 5px;
font-size: 14px;
z-index: 10;
}
a {
text-decoration: none;
color: #543f20;
}
a:hover {
color: #fff;
transition: all 0.6s ease;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment