Skip to content

Commit f2777ed

Browse files
authored
Merge pull request #7720 from limzykenneth/2.0-image
[p5.js 2.0] Refactor out reference to fn in p5.Image class
2 parents ff74e12 + 46e939d commit f2777ed

File tree

5 files changed

+378
-46
lines changed

5 files changed

+378
-46
lines changed

Diff for: src/image/image.js

-2
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,6 @@ function image(p5, fn){
272272
* @param {String} [extension]
273273
*/
274274
fn.saveCanvas = function(...args) {
275-
// p5._validateParameters('saveCanvas', args);
276-
277275
// copy arguments to array
278276
let htmlCanvas, filename, extension, temporaryGraphics;
279277

Diff for: src/image/p5.Image.js

+339-9
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
*/
1313
import Filters from './filters';
1414
import { Renderer } from '../core/p5.Renderer';
15-
16-
let fnRef;
15+
import { downloadFile, _checkFileExtension } from '../io/utilities';
1716

1817
class Image {
1918
constructor(width, height) {
@@ -941,7 +940,87 @@ class Image {
941940
* @param {Integer} dh
942941
*/
943942
copy(...args) {
944-
fnRef.copy.apply(this, args);
943+
// NOTE: Duplicate implementation here and pixels.js
944+
let srcImage, sx, sy, sw, sh, dx, dy, dw, dh;
945+
if (args.length === 9) {
946+
srcImage = args[0];
947+
sx = args[1];
948+
sy = args[2];
949+
sw = args[3];
950+
sh = args[4];
951+
dx = args[5];
952+
dy = args[6];
953+
dw = args[7];
954+
dh = args[8];
955+
} else if (args.length === 8) {
956+
srcImage = this;
957+
sx = args[0];
958+
sy = args[1];
959+
sw = args[2];
960+
sh = args[3];
961+
dx = args[4];
962+
dy = args[5];
963+
dw = args[6];
964+
dh = args[7];
965+
} else {
966+
throw new Error('Signature not supported');
967+
}
968+
969+
this._copyHelper(this, srcImage, sx, sy, sw, sh, dx, dy, dw, dh);
970+
}
971+
972+
_copyHelper(
973+
dstImage,
974+
srcImage,
975+
sx,
976+
sy,
977+
sw,
978+
sh,
979+
dx,
980+
dy,
981+
dw,
982+
dh
983+
){
984+
const s = srcImage.canvas.width / srcImage.width;
985+
// adjust coord system for 3D when renderer
986+
// ie top-left = -width/2, -height/2
987+
let sxMod = 0;
988+
let syMod = 0;
989+
if (srcImage._renderer && srcImage._renderer.isP3D) {
990+
sxMod = srcImage.width / 2;
991+
syMod = srcImage.height / 2;
992+
}
993+
if (dstImage._renderer && dstImage._renderer.isP3D) {
994+
dstImage.push();
995+
dstImage.resetMatrix();
996+
dstImage.noLights();
997+
dstImage.blendMode(dstImage.BLEND);
998+
dstImage.imageMode(dstImage.CORNER);
999+
dstImage._renderer.image(
1000+
srcImage,
1001+
sx + sxMod,
1002+
sy + syMod,
1003+
sw,
1004+
sh,
1005+
dx,
1006+
dy,
1007+
dw,
1008+
dh
1009+
);
1010+
dstImage.pop();
1011+
} else {
1012+
dstImage.drawingContext.drawImage(
1013+
srcImage.canvas,
1014+
s * (sx + sxMod),
1015+
s * (sy + syMod),
1016+
s * sw,
1017+
s * sh,
1018+
dx,
1019+
dy,
1020+
dw,
1021+
dh
1022+
);
1023+
}
9451024
}
9461025

9471026
/**
@@ -1374,8 +1453,13 @@ class Image {
13741453
* @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode
13751454
*/
13761455
blend(...args) {
1377-
// p5._validateParameters('p5.Image.blend', arguments);
1378-
fnRef.blend.apply(this, args);
1456+
const currBlend = this.drawingContext.globalCompositeOperation;
1457+
const blendMode = args[args.length - 1];
1458+
const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1);
1459+
1460+
this.drawingContext.globalCompositeOperation = blendMode;
1461+
this.copy(...copyArgs);
1462+
this.drawingContext.globalCompositeOperation = currBlend;
13791463
this.setModified(true);
13801464
}
13811465

@@ -1461,9 +1545,32 @@ class Image {
14611545
*/
14621546
save(filename, extension) {
14631547
if (this.gifProperties) {
1464-
fnRef.encodeAndDownloadGif(this, filename);
1548+
encodeAndDownloadGif(this, filename);
14651549
} else {
1466-
fnRef.saveCanvas(this.canvas, filename, extension);
1550+
let htmlCanvas = this.canvas;
1551+
extension =
1552+
extension ||
1553+
_checkFileExtension(filename, extension)[1] ||
1554+
'png';
1555+
1556+
let mimeType;
1557+
switch (extension) {
1558+
default:
1559+
//case 'png':
1560+
mimeType = 'image/png';
1561+
break;
1562+
case 'webp':
1563+
mimeType = 'image/webp';
1564+
break;
1565+
case 'jpeg':
1566+
case 'jpg':
1567+
mimeType = 'image/jpeg';
1568+
break;
1569+
}
1570+
1571+
htmlCanvas.toBlob(blob => {
1572+
downloadFile(blob, filename, extension);
1573+
}, mimeType);
14671574
}
14681575
}
14691576

@@ -1821,9 +1928,232 @@ class Image {
18211928
}
18221929
};
18231930

1824-
function image(p5, fn){
1825-
fnRef = fn;
1931+
function encodeAndDownloadGif(pImg, filename) {
1932+
const props = pImg.gifProperties;
1933+
1934+
//convert loopLimit back into Netscape Block formatting
1935+
let loopLimit = props.loopLimit;
1936+
if (loopLimit === 1) {
1937+
loopLimit = null;
1938+
} else if (loopLimit === null) {
1939+
loopLimit = 0;
1940+
}
1941+
const buffer = new Uint8Array(pImg.width * pImg.height * props.numFrames);
18261942

1943+
const allFramesPixelColors = [];
1944+
1945+
// Used to determine the occurrence of unique palettes and the frames
1946+
// which use them
1947+
const paletteFreqsAndFrames = {};
1948+
1949+
// Pass 1:
1950+
//loop over frames and get the frequency of each palette
1951+
for (let i = 0; i < props.numFrames; i++) {
1952+
const paletteSet = new Set();
1953+
const data = props.frames[i].image.data;
1954+
const dataLength = data.length;
1955+
// The color for each pixel in this frame ( for easier lookup later )
1956+
const pixelColors = new Uint32Array(pImg.width * pImg.height);
1957+
for (let j = 0, k = 0; j < dataLength; j += 4, k++) {
1958+
const r = data[j + 0];
1959+
const g = data[j + 1];
1960+
const b = data[j + 2];
1961+
const color = (r << 16) | (g << 8) | (b << 0);
1962+
paletteSet.add(color);
1963+
1964+
// What color does this pixel have in this frame ?
1965+
pixelColors[k] = color;
1966+
}
1967+
1968+
// A way to put use the entire palette as an object key
1969+
const paletteStr = [...paletteSet].sort().toString();
1970+
if (paletteFreqsAndFrames[paletteStr] === undefined) {
1971+
paletteFreqsAndFrames[paletteStr] = { freq: 1, frames: [i] };
1972+
} else {
1973+
paletteFreqsAndFrames[paletteStr].freq += 1;
1974+
paletteFreqsAndFrames[paletteStr].frames.push(i);
1975+
}
1976+
1977+
allFramesPixelColors.push(pixelColors);
1978+
}
1979+
1980+
let framesUsingGlobalPalette = [];
1981+
1982+
// Now to build the global palette
1983+
// Sort all the unique palettes in descending order of their occurrence
1984+
const palettesSortedByFreq = Object.keys(paletteFreqsAndFrames).sort(function(
1985+
a,
1986+
b
1987+
) {
1988+
return paletteFreqsAndFrames[b].freq - paletteFreqsAndFrames[a].freq;
1989+
});
1990+
1991+
// The initial global palette is the one with the most occurrence
1992+
const globalPalette = palettesSortedByFreq[0]
1993+
.split(',')
1994+
.map(a => parseInt(a));
1995+
1996+
framesUsingGlobalPalette = framesUsingGlobalPalette.concat(
1997+
paletteFreqsAndFrames[globalPalette].frames
1998+
);
1999+
2000+
const globalPaletteSet = new Set(globalPalette);
2001+
2002+
// Build a more complete global palette
2003+
// Iterate over the remaining palettes in the order of
2004+
// their occurrence and see if the colors in this palette which are
2005+
// not in the global palette can be added there, while keeping the length
2006+
// of the global palette <= 256
2007+
for (let i = 1; i < palettesSortedByFreq.length; i++) {
2008+
const palette = palettesSortedByFreq[i].split(',').map(a => parseInt(a));
2009+
2010+
const difference = palette.filter(x => !globalPaletteSet.has(x));
2011+
if (globalPalette.length + difference.length <= 256) {
2012+
for (let j = 0; j < difference.length; j++) {
2013+
globalPalette.push(difference[j]);
2014+
globalPaletteSet.add(difference[j]);
2015+
}
2016+
2017+
// All frames using this palette now use the global palette
2018+
framesUsingGlobalPalette = framesUsingGlobalPalette.concat(
2019+
paletteFreqsAndFrames[palettesSortedByFreq[i]].frames
2020+
);
2021+
}
2022+
}
2023+
2024+
framesUsingGlobalPalette = new Set(framesUsingGlobalPalette);
2025+
2026+
// Build a lookup table of the index of each color in the global palette
2027+
// Maps a color to its index
2028+
const globalIndicesLookup = {};
2029+
for (let i = 0; i < globalPalette.length; i++) {
2030+
if (!globalIndicesLookup[globalPalette[i]]) {
2031+
globalIndicesLookup[globalPalette[i]] = i;
2032+
}
2033+
}
2034+
2035+
// force palette to be power of 2
2036+
let powof2 = 1;
2037+
while (powof2 < globalPalette.length) {
2038+
powof2 <<= 1;
2039+
}
2040+
globalPalette.length = powof2;
2041+
2042+
// global opts
2043+
const opts = {
2044+
loop: loopLimit,
2045+
palette: new Uint32Array(globalPalette)
2046+
};
2047+
const gifWriter = new omggif.GifWriter(buffer, pImg.width, pImg.height, opts);
2048+
let previousFrame = {};
2049+
2050+
// Pass 2
2051+
// Determine if the frame needs a local palette
2052+
// Also apply transparency optimization. This function will often blow up
2053+
// the size of a GIF if not for transparency. If a pixel in one frame has
2054+
// the same color in the previous frame, that pixel can be marked as
2055+
// transparent. We decide one particular color as transparent and make all
2056+
// transparent pixels take this color. This helps in later in compression.
2057+
for (let i = 0; i < props.numFrames; i++) {
2058+
const localPaletteRequired = !framesUsingGlobalPalette.has(i);
2059+
const palette = localPaletteRequired ? [] : globalPalette;
2060+
const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height);
2061+
2062+
// Lookup table mapping color to its indices
2063+
const colorIndicesLookup = {};
2064+
2065+
// All the colors that cannot be marked transparent in this frame
2066+
const cannotBeTransparent = new Set();
2067+
2068+
allFramesPixelColors[i].forEach((color, k) => {
2069+
if (localPaletteRequired) {
2070+
if (colorIndicesLookup[color] === undefined) {
2071+
colorIndicesLookup[color] = palette.length;
2072+
palette.push(color);
2073+
}
2074+
pixelPaletteIndex[k] = colorIndicesLookup[color];
2075+
} else {
2076+
pixelPaletteIndex[k] = globalIndicesLookup[color];
2077+
}
2078+
2079+
if (i > 0) {
2080+
// If even one pixel of this color has changed in this frame
2081+
// from the previous frame, we cannot mark it as transparent
2082+
if (allFramesPixelColors[i - 1][k] !== color) {
2083+
cannotBeTransparent.add(color);
2084+
}
2085+
}
2086+
});
2087+
2088+
const frameOpts = {};
2089+
2090+
// Transparency optimization
2091+
const canBeTransparent = palette.filter(a => !cannotBeTransparent.has(a));
2092+
if (canBeTransparent.length > 0) {
2093+
// Select a color to mark as transparent
2094+
const transparent = canBeTransparent[0];
2095+
const transparentIndex = localPaletteRequired
2096+
? colorIndicesLookup[transparent]
2097+
: globalIndicesLookup[transparent];
2098+
if (i > 0) {
2099+
for (let k = 0; k < allFramesPixelColors[i].length; k++) {
2100+
// If this pixel in this frame has the same color in previous frame
2101+
if (allFramesPixelColors[i - 1][k] === allFramesPixelColors[i][k]) {
2102+
pixelPaletteIndex[k] = transparentIndex;
2103+
}
2104+
}
2105+
frameOpts.transparent = transparentIndex;
2106+
// If this frame has any transparency, do not dispose the previous frame
2107+
previousFrame.frameOpts.disposal = 1;
2108+
}
2109+
}
2110+
frameOpts.delay = props.frames[i].delay / 10; // Move timing back into GIF formatting
2111+
if (localPaletteRequired) {
2112+
// force palette to be power of 2
2113+
let powof2 = 1;
2114+
while (powof2 < palette.length) {
2115+
powof2 <<= 1;
2116+
}
2117+
palette.length = powof2;
2118+
frameOpts.palette = new Uint32Array(palette);
2119+
}
2120+
if (i > 0) {
2121+
// add the frame that came before the current one
2122+
gifWriter.addFrame(
2123+
0,
2124+
0,
2125+
pImg.width,
2126+
pImg.height,
2127+
previousFrame.pixelPaletteIndex,
2128+
previousFrame.frameOpts
2129+
);
2130+
}
2131+
// previous frame object should now have details of this frame
2132+
previousFrame = {
2133+
pixelPaletteIndex,
2134+
frameOpts
2135+
};
2136+
}
2137+
2138+
previousFrame.frameOpts.disposal = 1;
2139+
// add the last frame
2140+
gifWriter.addFrame(
2141+
0,
2142+
0,
2143+
pImg.width,
2144+
pImg.height,
2145+
previousFrame.pixelPaletteIndex,
2146+
previousFrame.frameOpts
2147+
);
2148+
2149+
const extension = 'gif';
2150+
const blob = new Blob([buffer.slice(0, gifWriter.end())], {
2151+
type: 'image/gif'
2152+
});
2153+
downloadFile(blob, filename, extension);
2154+
};
2155+
2156+
function image(p5, fn){
18272157
/**
18282158
* A class to describe an image.
18292159
*

0 commit comments

Comments
 (0)