diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm
index 86a1449754..75adf02a7c 100644
--- a/wled00/data/pixelforge/pixelforge.htm
+++ b/wled00/data/pixelforge/pixelforge.htm
@@ -25,13 +25,13 @@
}
/* shimmer text animation */
.title .sh {
- background: linear-gradient(90deg,
- #7b47db 0%, #ff6b6b 20%, #feca57 40%, #48dbfb 60%, #7b47db 100%);
- background-size: 200% 100%;
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: shimmer 4s ease-in-out 5;
- font-size: 36px;
+ background: linear-gradient(90deg,
+ #7b47db 0%, #ff6b6b 20%, #feca57 40%, #48dbfb 60%, #7b47db 100%);
+ background-size: 200% 100%;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: shimmer 4s ease-in-out 5;
+ font-size: 36px;
}
@keyframes shimmer { 50% { background-position: 600% 0; } }
@@ -278,11 +278,15 @@
Preview at target resolution
@@ -430,9 +434,11 @@
PIXEL MAGIC Tool
/* canvases */
const cv=gId('cv'),cx=cv.getContext('2d',{willReadFrequently:true});
const pv=gId('pv'),pvx=pv.getContext('2d',{willReadFrequently:true});
+const rv = cE('canvas'), rvc = rv.getContext('2d',{willReadFrequently:true}); // off screen canvas for drawing resized & rotated image
+rv.width = cv.width; rv.height = cv.height;
/* globals */
-let wu='',sI=null,sF=null,cI=null,bS=1,iS=1,pX=0,pY=0;
+let wu='',sI=null,sF=null,cI=null,bS=1,iS=1,pX=0,pY=0,rot=0;
let cr={x:50,y:50,w:200,h:150},drag=false,dH=null,oX=0,oY=0;
let pan=false,psX=0,psY=0,poX=0,poY=0;
let iL=[]; // image list
@@ -774,6 +780,16 @@
PIXEL MAGIC Tool
crClamp(); crDraw();
};
+/* rotation */
+function rotUpd(v){
+ if(gId('snap').checked) v = Math.round(v/15)*15 % 360; // snap to multiples of 15°
+ rot = v;
+ gId('rotVal').textContent = v;
+ if(cI) crDraw();
+}
+gId('rotSl').oninput = ()=> rotUpd(+gId('rotSl').value);
+
+
/* color change */
gId('bg').oninput=crDraw;
@@ -882,12 +898,25 @@
PIXEL MAGIC Tool
/* draw + preview */
function crDraw(){
+ if(!cI) return;
+
+ // render rotated image to offscreen
+ rvc.clearRect(0,0,rv.width,rv.height);
+ rvc.fillStyle = gId('bg').value;
+ rvc.fillRect(0,0,rv.width,rv.height);
+ rvc.imageSmoothingEnabled = false;
+ rvc.save();
+ const dw = cI.width * iS, dh = cI.height * iS;
+ rvc.translate(pX + dw/2, pY + dh/2);
+ rvc.rotate(rot * Math.PI / 180);
+ rvc.drawImage(cI, -dw/2, -dh/2, dw, dh);
+ rvc.restore();
+
+ // copy offscreen to visible
cx.clearRect(0,0,cv.width,cv.height);
- if(!cI)return;
- cx.fillStyle=gId('bg').value; cx.fillRect(0,0,cv.width,cv.height);
- cx.imageSmoothingEnabled=false;
- cx.drawImage(cI,0,0,cI.width,cI.height,pX,pY,cI.width*iS,cI.height*iS);
- /* crop frame */
+ cx.drawImage(rv, 0, 0);
+
+ // overlay crop frame (only on visible)
cx.lineWidth=3; cx.setLineDash([6,4]); cx.shadowColor="#000"; cx.shadowBlur=2;
cx.strokeStyle="#FFF"; cx.beginPath(); cx.roundRect(cr.x,cr.y,cr.w,cr.h,6); cx.stroke();
cx.shadowColor="#000F";
@@ -913,7 +942,8 @@
PIXEL MAGIC Tool
const tcx = tc.getContext('2d');
tcx.fillStyle=gId('bg').value;
tcx.fillRect(0,0,w,h); // fill background (for transparent images)
- tcx.drawImage(cI,(cr.x-pX)/iS,(cr.y-pY)/iS,cr.w/iS,cr.h/iS,0,0,w,h);
+ tcx.imageSmoothingEnabled = false;
+ tcx.drawImage(rv, cr.x, cr.y, cr.w, cr.h, 0, 0, w, h); // sample cropped area from off screen canvas
blackTh(tcx);
// scale/stretch to preview canvas, limit to 256px in largest dimension but keep aspect ratio
const ratio = h/w;
@@ -1003,11 +1033,28 @@
PIXEL MAGIC Tool
const frames = [];
for (let i = 0; i < gF.length; i++) {
+ // put current GIF frame into tc
const id = new ImageData(new Uint8ClampedArray(gF[i].pixels), gI.width, gI.height);
tctx.putImageData(id, 0, 0);
+
+ // render this frame into the offscreen rotated canvas (no overlay)
+ rvc.clearRect(0, 0, rv.width, rv.height);
+ rvc.fillStyle = gId('bg').value;
+ rvc.fillRect(0, 0, rv.width, rv.height);
+ rvc.imageSmoothingEnabled = false;
+ rvc.save();
+ const dw = gI.width * iS, dh = gI.height * iS;
+ rvc.translate(pX + dw / 2, pY + dh / 2);
+ rvc.rotate(rot * Math.PI / 180);
+ rvc.drawImage(tc, -dw / 2, -dh / 2, dw, dh);
+ rvc.restore();
+
+ // sample the crop from the offscreen (already rotated) canvas into output size
cctx.fillStyle = gId('bg').value;
cctx.fillRect(0, 0, w, h);
- cctx.drawImage(tc, (cr.x - pX) / iS, (cr.y - pY) / iS, cr.w / iS, cr.h / iS, 0, 0, w, h);
+ cctx.imageSmoothingEnabled = false;
+ cctx.drawImage(rv, cr.x, cr.y, cr.w, cr.h, 0, 0, w, h);
+
blackTh(cctx);
const fd = cctx.getImageData(0, 0, w, h);
frames.push({ data: fd.data, delay: gF[i].delay });