adding image rotation to PixelForge gif tool (#5309)
This commit is contained in:
@@ -278,11 +278,15 @@ button, .btn {
|
||||
|
||||
<div class="cw">
|
||||
<div style="width:100%">
|
||||
<div class="slc">
|
||||
<label>Rotation: <span id="rotVal">0</span>° <input type="checkbox" id="snap">snap</label>
|
||||
<input type="range" id="rotSl" min="0" max="359" value="0" class="sl">
|
||||
</div>
|
||||
<div class="slc">
|
||||
<label>Zoom: </label>
|
||||
<input type="range" id="zoom" min="0" max="100" value="0" class="sl">
|
||||
</div>
|
||||
<canvas id="cv" width="500" height="400"></canvas>
|
||||
<canvas id="cv" width="500" height="500"></canvas>
|
||||
</div>
|
||||
|
||||
<small>Preview at target resolution</small>
|
||||
@@ -430,9 +434,11 @@ const txtFX = 122; // scrolling text effect number
|
||||
/* 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 @@ gId('zoom').oninput=()=>{
|
||||
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 @@ cv.ontouchcancel=e=>{e.preventDefault();actEnd();};
|
||||
|
||||
/* draw + preview */
|
||||
function crDraw(){
|
||||
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 */
|
||||
|
||||
// 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);
|
||||
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 @@ function prevUpd(){
|
||||
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 @@ gId('up').onclick = async () => {
|
||||
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user