adding image rotation to PixelForge gif tool (#5309)
This commit is contained in:
parent
4e072962c0
commit
857e73ab25
@ -25,13 +25,13 @@ h3 {
|
|||||||
}
|
}
|
||||||
/* shimmer text animation */
|
/* shimmer text animation */
|
||||||
.title .sh {
|
.title .sh {
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(90deg,
|
||||||
#7b47db 0%, #ff6b6b 20%, #feca57 40%, #48dbfb 60%, #7b47db 100%);
|
#7b47db 0%, #ff6b6b 20%, #feca57 40%, #48dbfb 60%, #7b47db 100%);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
animation: shimmer 4s ease-in-out 5;
|
animation: shimmer 4s ease-in-out 5;
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
}
|
}
|
||||||
@keyframes shimmer { 50% { background-position: 600% 0; } }
|
@keyframes shimmer { 50% { background-position: 600% 0; } }
|
||||||
|
|
||||||
@ -278,11 +278,15 @@ button, .btn {
|
|||||||
|
|
||||||
<div class="cw">
|
<div class="cw">
|
||||||
<div style="width:100%">
|
<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">
|
<div class="slc">
|
||||||
<label>Zoom: </label>
|
<label>Zoom: </label>
|
||||||
<input type="range" id="zoom" min="0" max="100" value="0" class="sl">
|
<input type="range" id="zoom" min="0" max="100" value="0" class="sl">
|
||||||
</div>
|
</div>
|
||||||
<canvas id="cv" width="500" height="400"></canvas>
|
<canvas id="cv" width="500" height="500"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small>Preview at target resolution</small>
|
<small>Preview at target resolution</small>
|
||||||
@ -430,9 +434,11 @@ const txtFX = 122; // scrolling text effect number
|
|||||||
/* canvases */
|
/* canvases */
|
||||||
const cv=gId('cv'),cx=cv.getContext('2d',{willReadFrequently:true});
|
const cv=gId('cv'),cx=cv.getContext('2d',{willReadFrequently:true});
|
||||||
const pv=gId('pv'),pvx=pv.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 */
|
/* 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 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 pan=false,psX=0,psY=0,poX=0,poY=0;
|
||||||
let iL=[]; // image list
|
let iL=[]; // image list
|
||||||
@ -774,6 +780,16 @@ gId('zoom').oninput=()=>{
|
|||||||
crClamp(); crDraw();
|
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 */
|
/* color change */
|
||||||
gId('bg').oninput=crDraw;
|
gId('bg').oninput=crDraw;
|
||||||
|
|
||||||
@ -882,12 +898,25 @@ cv.ontouchcancel=e=>{e.preventDefault();actEnd();};
|
|||||||
|
|
||||||
/* draw + preview */
|
/* draw + preview */
|
||||||
function crDraw(){
|
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);
|
cx.clearRect(0,0,cv.width,cv.height);
|
||||||
if(!cI)return;
|
cx.drawImage(rv, 0, 0);
|
||||||
cx.fillStyle=gId('bg').value; cx.fillRect(0,0,cv.width,cv.height);
|
|
||||||
cx.imageSmoothingEnabled=false;
|
// overlay crop frame (only on visible)
|
||||||
cx.drawImage(cI,0,0,cI.width,cI.height,pX,pY,cI.width*iS,cI.height*iS);
|
|
||||||
/* crop frame */
|
|
||||||
cx.lineWidth=3; cx.setLineDash([6,4]); cx.shadowColor="#000"; cx.shadowBlur=2;
|
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.strokeStyle="#FFF"; cx.beginPath(); cx.roundRect(cr.x,cr.y,cr.w,cr.h,6); cx.stroke();
|
||||||
cx.shadowColor="#000F";
|
cx.shadowColor="#000F";
|
||||||
@ -913,7 +942,8 @@ function prevUpd(){
|
|||||||
const tcx = tc.getContext('2d');
|
const tcx = tc.getContext('2d');
|
||||||
tcx.fillStyle=gId('bg').value;
|
tcx.fillStyle=gId('bg').value;
|
||||||
tcx.fillRect(0,0,w,h); // fill background (for transparent images)
|
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);
|
blackTh(tcx);
|
||||||
// scale/stretch to preview canvas, limit to 256px in largest dimension but keep aspect ratio
|
// scale/stretch to preview canvas, limit to 256px in largest dimension but keep aspect ratio
|
||||||
const ratio = h/w;
|
const ratio = h/w;
|
||||||
@ -1003,11 +1033,28 @@ gId('up').onclick = async () => {
|
|||||||
|
|
||||||
const frames = [];
|
const frames = [];
|
||||||
for (let i = 0; i < gF.length; i++) {
|
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);
|
const id = new ImageData(new Uint8ClampedArray(gF[i].pixels), gI.width, gI.height);
|
||||||
tctx.putImageData(id, 0, 0);
|
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.fillStyle = gId('bg').value;
|
||||||
cctx.fillRect(0, 0, w, h);
|
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);
|
blackTh(cctx);
|
||||||
const fd = cctx.getImageData(0, 0, w, h);
|
const fd = cctx.getImageData(0, 0, w, h);
|
||||||
frames.push({ data: fd.data, delay: gF[i].delay });
|
frames.push({ data: fd.data, delay: gF[i].delay });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user