|
12 | 12 | */
|
13 | 13 | import Filters from './filters';
|
14 | 14 | import { Renderer } from '../core/p5.Renderer';
|
15 |
| - |
16 |
| -let fnRef; |
| 15 | +import { downloadFile, _checkFileExtension } from '../io/utilities'; |
17 | 16 |
|
18 | 17 | class Image {
|
19 | 18 | constructor(width, height) {
|
@@ -941,7 +940,87 @@ class Image {
|
941 | 940 | * @param {Integer} dh
|
942 | 941 | */
|
943 | 942 | 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 | + } |
945 | 1024 | }
|
946 | 1025 |
|
947 | 1026 | /**
|
@@ -1374,8 +1453,13 @@ class Image {
|
1374 | 1453 | * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode
|
1375 | 1454 | */
|
1376 | 1455 | 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; |
1379 | 1463 | this.setModified(true);
|
1380 | 1464 | }
|
1381 | 1465 |
|
@@ -1461,9 +1545,32 @@ class Image {
|
1461 | 1545 | */
|
1462 | 1546 | save(filename, extension) {
|
1463 | 1547 | if (this.gifProperties) {
|
1464 |
| - fnRef.encodeAndDownloadGif(this, filename); |
| 1548 | + encodeAndDownloadGif(this, filename); |
1465 | 1549 | } 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); |
1467 | 1574 | }
|
1468 | 1575 | }
|
1469 | 1576 |
|
@@ -1821,9 +1928,232 @@ class Image {
|
1821 | 1928 | }
|
1822 | 1929 | };
|
1823 | 1930 |
|
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); |
1826 | 1942 |
|
| 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){ |
1827 | 2157 | /**
|
1828 | 2158 | * A class to describe an image.
|
1829 | 2159 | *
|
|
0 commit comments