From 5993cd14474261121460a8ced448db3780f7fa76 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 19 Sep 2021 09:53:15 -0400 Subject: [PATCH 001/177] Re-implement _getTintedImageCanvas and add tests --- src/core/p5.Renderer2D.js | 87 ++++++++++----- src/image/loading_displaying.js | 30 +---- .../tint-performance/flowers-large.jpg | Bin 0 -> 82652 bytes .../tint-performance/index.html | 7 ++ .../tint-performance/sketch.js | 54 +++++++++ test/unit/assets/cat-with-hole.png | Bin 0 -> 85478 bytes test/unit/image/loading.js | 103 ++++++++++++++++++ 7 files changed, 227 insertions(+), 54 deletions(-) create mode 100644 test/manual-test-examples/tint-performance/flowers-large.jpg create mode 100644 test/manual-test-examples/tint-performance/index.html create mode 100644 test/manual-test-examples/tint-performance/sketch.js create mode 100644 test/unit/assets/cat-with-hole.png diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 345e7d9cbe..56c1a137c3 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1,6 +1,5 @@ import p5 from './main'; import * as constants from './constants'; -import filters from '../image/filters'; import './p5.Renderer'; @@ -155,13 +154,8 @@ p5.Renderer2D.prototype.image = function( } try { - if (this._tint) { - if (p5.MediaElement && img instanceof p5.MediaElement) { - img.loadPixels(); - } - if (img.canvas) { - cnv = this._getTintedImageCanvas(img); - } + if (this._tint && img.canvas) { + cnv = this._getTintedImageCanvas(img); } if (!cnv) { cnv = img.canvas || img.elt; @@ -198,25 +192,66 @@ p5.Renderer2D.prototype._getTintedImageCanvas = function(img) { if (!img.canvas) { return img; } - const pixels = filters._toPixels(img.canvas); - const tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = img.canvas.width; - tmpCanvas.height = img.canvas.height; - const tmpCtx = tmpCanvas.getContext('2d'); - const id = tmpCtx.createImageData(img.canvas.width, img.canvas.height); - const newPixels = id.data; - for (let i = 0; i < pixels.length; i += 4) { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const a = pixels[i + 3]; - newPixels[i] = r * this._tint[0] / 255; - newPixels[i + 1] = g * this._tint[1] / 255; - newPixels[i + 2] = b * this._tint[2] / 255; - newPixels[i + 3] = a * this._tint[3] / 255; + + if (!img.tintCanvas) { + // Once an image has been tinted, keep its tint canvas + // around so we don't need to re-incur the cost of + // creating a new one for each tint + img.tintCanvas = document.createElement('canvas'); + } + + // Keep the size of the tint canvas up-to-date + if (img.tintCanvas.width !== img.canvas.width) { + img.tintCanvas.width = img.canvas.width; + } + if (img.tintCanvas.height !== img.canvas.height) { + img.tintCanvas.height = img.canvas.height; } - tmpCtx.putImageData(id, 0, 0); - return tmpCanvas; + + // Goal: multiply the r,g,b,a values of the source by + // the r,g,b,a values of the tint color + const ctx = img.tintCanvas.getContext('2d'); + + ctx.save(); + ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); + + if (this._tint[0] < 255 || this._tint[1] < 255 || this._tint[2] < 255) { + // Color tint: we need to use the multiply blend mode to change the colors. + // However, the canvas implementation of this destroys the alpha channel of + // the image. To accommodate, we first get a version of the image with full + // opacity everywhere, tint using multiply, and then use the destination-in + // blend mode to restore the alpha channel again. + + // Start with the original image + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode makes everything opaque but forces the luma to match + // the original image again + ctx.globalCompositeOperation = 'luminosity'; + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode forces the hue and chroma to match the original image. + // After this we should have the original again, but with full opacity. + ctx.globalCompositeOperation = 'color'; + ctx.drawImage(img.canvas, 0, 0); + + // Apply color tint + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = `rgb(${this._tint.slice(0, 3).join(', ')})`; + ctx.fillRect(0, 0, img.canvas.width, img.canvas.height); + + // Replace the alpha channel with the original alpha * the alpha tint + ctx.globalCompositeOperation = 'destination-in'; + ctx.globalAlpha = this._tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); + } else { + // If we only need to change the alpha, we can skip all the extra work! + ctx.globalAlpha = this._tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); + } + + ctx.restore(); + return img.tintCanvas; }; ////////////////////////////////////////////// diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index f961740e89..5f3b3d7c13 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -6,7 +6,6 @@ */ import p5 from '../core/main'; -import Filters from './filters'; import canvas from '../core/helpers'; import * as constants from '../core/constants'; import omggif from 'omggif'; @@ -602,33 +601,8 @@ p5.prototype.noTint = function() { * @param {p5.Image} The image to be tinted * @return {canvas} The resulting tinted canvas */ -p5.prototype._getTintedImageCanvas = function(img) { - if (!img.canvas) { - return img; - } - const pixels = Filters._toPixels(img.canvas); - const tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = img.canvas.width; - tmpCanvas.height = img.canvas.height; - const tmpCtx = tmpCanvas.getContext('2d'); - const id = tmpCtx.createImageData(img.canvas.width, img.canvas.height); - const newPixels = id.data; - - for (let i = 0; i < pixels.length; i += 4) { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const a = pixels[i + 3]; - - newPixels[i] = r * this._renderer._tint[0] / 255; - newPixels[i + 1] = g * this._renderer._tint[1] / 255; - newPixels[i + 2] = b * this._renderer._tint[2] / 255; - newPixels[i + 3] = a * this._renderer._tint[3] / 255; - } - - tmpCtx.putImageData(id, 0, 0); - return tmpCanvas; -}; +p5.prototype._getTintedImageCanvas = + p5.Renderer2D.prototype._getTintedImageCanvas; /** * Set image mode. Modifies the location from which images are drawn by diff --git a/test/manual-test-examples/tint-performance/flowers-large.jpg b/test/manual-test-examples/tint-performance/flowers-large.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a54909cebfde78d60bbe1ebe18fc5a7d427cea0 GIT binary patch literal 82652 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%0A%^TdhW1Ugj-4bW+4br#;NF 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 Oo7(|&{~W?U5B?AQMYdP~ literal 0 HcmV?d00001 diff --git a/test/manual-test-examples/tint-performance/index.html b/test/manual-test-examples/tint-performance/index.html new file mode 100644 index 0000000000..8624ec9ae5 --- /dev/null +++ b/test/manual-test-examples/tint-performance/index.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/tint-performance/sketch.js b/test/manual-test-examples/tint-performance/sketch.js new file mode 100644 index 0000000000..34666a31fb --- /dev/null +++ b/test/manual-test-examples/tint-performance/sketch.js @@ -0,0 +1,54 @@ +var img; +var times = []; + +function preload() { + img = loadImage('flowers-large.jpg'); +} + +function setup() { + createCanvas(800, 160); +} + +function drawScaledImage(img, x, y) { + push(); + translate(x, y); + scale(0.125); + image(img, 0, 0); + pop(); +} + +function draw() { + times.push(deltaTime); + if (times.length > 60) { + times.shift(); + } + const avgDelta = + times.reduce(function(acc, next) { + return acc + next; + }) / times.length; + const avgRate = 1000 / avgDelta; + + clear(); + push(); + translate(50 * sin(millis() / 1000), 50 * cos(millis() / 1000)); + fill(255, 255, 255); + rect(0, 0, 480, 160); + drawScaledImage(img, 0, 0); + tint(0, 0, 150, 150); // Tint alpha blue + drawScaledImage(img, 160, 0); + tint(255, 255, 255); + drawScaledImage(img, 320, 0); + tint(0, 153, 150); // Tint turquoise + drawScaledImage(img, 480, 0); + noTint(); + drawScaledImage(img, 640, 0); + pop(); + + push(); + textAlign(LEFT, TOP); + textSize(20); + noStroke(); + fill(0); + text(avgRate.toFixed(2) + ' FPS', 10, 10); + pop(); +} diff --git a/test/unit/assets/cat-with-hole.png b/test/unit/assets/cat-with-hole.png new file mode 100644 index 0000000000000000000000000000000000000000..249b38010cd13061ecbe6a48019217b5298eded2 GIT binary patch literal 85478 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 Date: Tue, 7 Dec 2021 10:08:14 -0500 Subject: [PATCH 002/177] Fixes #5207 --- src/dom/dom.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/dom/dom.js b/src/dom/dom.js index 4a8d2549d5..ce5de2be9d 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -826,19 +826,32 @@ p5.prototype.createRadio = function() { // If already given with a containerEl, will search for all input[radio] // it, create a p5.Element out of it, add options to it and return the p5.Element. + let self; let radioElement; let name; const arg0 = arguments[0]; - // If existing radio Element is provided as argument 0 - if (arg0 instanceof HTMLDivElement || arg0 instanceof HTMLSpanElement) { + if ( + arg0 instanceof p5.Element && + (arg0.elt instanceof HTMLDivElement || arg0.elt instanceof HTMLSpanElement) + ) { + // If given argument is p5.Element of div/span type + self = arg0; + this.elt = arg0.elt; + } else if ( + // If existing radio Element is provided as argument 0 + arg0 instanceof HTMLDivElement || + arg0 instanceof HTMLSpanElement + ) { + self = addElement(arg0, this); + this.elt = arg0; radioElement = arg0; if (typeof arguments[1] === 'string') name = arguments[1]; } else { if (typeof arg0 === 'string') name = arg0; radioElement = document.createElement('div'); + self = addElement(radioElement, this); + this.elt = radioElement; } - this.elt = radioElement; - let self = addElement(radioElement, this); self._name = name || 'radioOption'; // setup member functions From fbe4ed6ac877348753e90c222b765ef8fe0301ee Mon Sep 17 00:00:00 2001 From: Aaron Welles Date: Wed, 8 Dec 2021 21:32:45 -0500 Subject: [PATCH 003/177] fix #5497 --- src/math/p5.Vector.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 037a1524a3..6c1d70345f 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1489,9 +1489,11 @@ p5.Vector.prototype.heading = function heading() { */ p5.Vector.prototype.setHeading = function setHeading(a) { + let newHeading = a; + if (this.isPInst) newHeading = this._toRadians(newHeading); let m = this.mag(); - this.x = m * Math.cos(a); - this.y = m * Math.sin(a); + this.x = m * Math.cos(newHeading); + this.y = m * Math.sin(newHeading); return this; }; From 25a1b1c432ea002ced4bde0d2a7f3490fea5d8bb Mon Sep 17 00:00:00 2001 From: A Welles Date: Fri, 17 Dec 2021 18:31:35 -0500 Subject: [PATCH 004/177] Revert "fix #5497" This reverts commit fbe4ed6ac877348753e90c222b765ef8fe0301ee. --- src/math/p5.Vector.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 6c1d70345f..037a1524a3 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1489,11 +1489,9 @@ p5.Vector.prototype.heading = function heading() { */ p5.Vector.prototype.setHeading = function setHeading(a) { - let newHeading = a; - if (this.isPInst) newHeading = this._toRadians(newHeading); let m = this.mag(); - this.x = m * Math.cos(newHeading); - this.y = m * Math.sin(newHeading); + this.x = m * Math.cos(a); + this.y = m * Math.sin(a); return this; }; From 4828bfa06e3dacb8fafb15369288928e15172a3d Mon Sep 17 00:00:00 2001 From: Yifan Mai Date: Mon, 27 Dec 2021 17:06:49 -0800 Subject: [PATCH 005/177] Animated GIF masking resolves #5174 When the mask method is called on an Image that contains an animated GIF, the mask is applied to all of its frames. --- src/image/p5.Image.js | 31 ++++++++++++++--- test/unit/image/p5.Image.js | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index c78fa7959c..53b77750f6 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -629,10 +629,6 @@ p5.Image.prototype.copy = function(...args) { * http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/ */ // TODO: - Accept an array of alpha values. -// - Use other channels of an image. p5 uses the -// blue channel (which feels kind of arbitrary). Note: at the -// moment this method does not match native processing's original -// functionality exactly. p5.Image.prototype.mask = function(p5Image) { if (p5Image === undefined) { p5Image = this; @@ -657,7 +653,32 @@ p5.Image.prototype.mask = function(p5Image) { ]; this.drawingContext.globalCompositeOperation = 'destination-in'; - p5.Image.prototype.copy.apply(this, copyArgs); + if (this.gifProperties) { + const prevFrameData = this.drawingContext.getImageData( + 0, + 0, + this.width, + this.height + ); + for (let i = 0; i < this.gifProperties.frames.length; i++) { + this.drawingContext.clearRect(0, 0, this.width, this.height); + this.drawingContext.putImageData( + this.gifProperties.frames[i].image, + 0, + 0 + ); + p5.Image.prototype.copy.apply(this, copyArgs); + this.gifProperties.frames[i].image = this.drawingContext.getImageData( + 0, + 0, + this.width, + this.height + ); + } + this.drawingContext.putImageData(prevFrameData, 0, 0); + } else { + p5.Image.prototype.copy.apply(this, copyArgs); + } this.drawingContext.globalCompositeOperation = currBlend; this.setModified(true); }; diff --git a/test/unit/image/p5.Image.js b/test/unit/image/p5.Image.js index eb83102d85..8b30802d15 100644 --- a/test/unit/image/p5.Image.js +++ b/test/unit/image/p5.Image.js @@ -49,4 +49,70 @@ suite('p5.Image', function() { assert.strictEqual(img.height, 30); }); }); + + suite('p5.Image.prototype.mask', function() { + test('it should mask the image', function() { + let img = myp5.createImage(10, 10); + img.loadPixels(); + for (let i = 0; i < img.height; i++) { + for (let j = 0; j < img.width; j++) { + let alpha = i < 5 ? 255 : 0; + img.set(i, j, myp5.color(0, 0, 0, alpha)); + } + } + img.updatePixels(); + + let mask = myp5.createImage(10, 10); + mask.loadPixels(); + for (let i = 0; i < mask.width; i++) { + for (let j = 0; j < mask.height; j++) { + let alpha = j < 5 ? 255 : 0; + mask.set(i, j, myp5.color(0, 0, 0, alpha)); + } + } + mask.updatePixels(); + + img.mask(mask); + img.loadPixels(); + for (let i = 0; i < img.width; i++) { + for (let j = 0; j < img.height; j++) { + let alpha = i < 5 && j < 5 ? 255 : 0; + assert.strictEqual(img.get(i, j)[3], alpha); + } + } + }); + + test('it should mask the animated gif image', function() { + const imagePath = 'unit/assets/nyan_cat.gif'; + return new Promise(function(resolve, reject) { + myp5.loadImage(imagePath, resolve, reject); + }).then(function(img) { + let mask = myp5.createImage(img.width, img.height); + mask.loadPixels(); + for (let i = 0; i < mask.width; i++) { + for (let j = 0; j < mask.height; j++) { + const alpha = j < img.height < 2 ? 255 : 0; + mask.set(i, j, myp5.color(0, 0, 0, alpha)); + } + } + mask.updatePixels(); + + img.mask(mask); + for ( + frameIndex = 0; + frameIndex < img.gifProperties.numFrames; + frameIndex++ + ) { + const frameData = img.gifProperties.frames[frameIndex].image.data; + for (let i = 0; i < img.width; i++) { + for (let j = 0; j < img.height; j++) { + const index = 4 * (i + j * img.width) + 3; + const alpha = j < img.height < 2 ? 255 : 0; + assert.strictEqual(frameData[index], alpha); + } + } + } + }); + }); + }); }); From c02e933ac0d1c659eca8f9fd71f594ddcab512dc Mon Sep 17 00:00:00 2001 From: stampyzfanz <34364128+stampyzfanz@users.noreply.github.com> Date: Sat, 19 Mar 2022 11:38:03 +1100 Subject: [PATCH 006/177] Reword first issue welcome comment to mention issue forms. Fixes #5637 --- .github/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/config.yml b/.github/config.yml index 706ea19698..1d0f4cae3d 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -4,7 +4,7 @@ # Comment to be posted to on first time issues newIssueWelcomeComment: > - Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to follow the issue template if you haven't already. + Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to be sure to fill out the issue form template inputs accurately if you haven't already. # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome From e89a292c7144ee856fe364a10d5a65e892961c07 Mon Sep 17 00:00:00 2001 From: KevinGrajeda Date: Fri, 27 May 2022 18:16:36 -0500 Subject: [PATCH 007/177] add parameter validation and return to angleMode --- src/math/trigonometry.js | 13 +++++++++++-- test/unit/math/trigonometry.js | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index dfbbfc1cb0..979189d9f3 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -281,9 +281,10 @@ p5.prototype.radians = angle => angle * constants.DEG_TO_RAD; /** * Sets the current mode of p5 to the given mode. Default mode is RADIANS. * + * Calling angleMode() with no arguments returns current anglemode. * @method angleMode * @param {Constant} mode either RADIANS or DEGREES - * + * @chainable * @example *

* @@ -306,10 +307,18 @@ p5.prototype.radians = angle => angle * constants.DEG_TO_RAD; *
* */ +/** + * @method angleMode + * @return {Constant} mode either RADIANS or DEGREES + */ p5.prototype.angleMode = function(mode) { - if (mode === constants.DEGREES || mode === constants.RADIANS) { + p5._validateParameters('angleMode', arguments); + if (typeof mode === 'undefined') { + return this._angleMode; + } else if (mode === constants.DEGREES || mode === constants.RADIANS) { this._angleMode = mode; } + return this; }; /** diff --git a/test/unit/math/trigonometry.js b/test/unit/math/trigonometry.js index 437945f471..47d619fb15 100644 --- a/test/unit/math/trigonometry.js +++ b/test/unit/math/trigonometry.js @@ -60,6 +60,16 @@ suite('Trigonometry', function() { myp5.angleMode('wtflolzkk'); assert.equal(myp5._angleMode, 'radians'); }); + + test('should return radians', function() { + myp5.angleMode(RADIANS); + assert.equal(myp5.angleMode(), 'radians'); + }); + + test('should return degrees', function() { + myp5.angleMode(DEGREES); + assert.equal(myp5.angleMode(), 'degrees'); + }); }); suite('p5.prototype.degrees', function() { From 976a3e12c893dabdcf25627006b3f8b8711fab74 Mon Sep 17 00:00:00 2001 From: Yash Lamba Date: Sat, 28 May 2022 12:05:56 +0530 Subject: [PATCH 008/177] Fixed background transparency with Images Co-authored-by: TwoTicks --- src/core/p5.Renderer2D.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 345e7d9cbe..e9a45fc5c0 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -47,7 +47,14 @@ p5.Renderer2D.prototype.background = function(...args) { this.resetMatrix(); if (args[0] instanceof p5.Image) { - this._pInst.image(args[0], 0, 0, this.width, this.height); + if (args[1] >= 0) { + // set transparency of background + const img = args[0]; + this.drawingContext.globalAlpha = args[1] / 255; + this._pInst.image(img, 0, 0, this.width, this.height); + } else { + this._pInst.image(args[0], 0, 0, this.width, this.height); + } } else { const curFill = this._getFill(); // create background rect From aa4de60c1ac340d95063043bf7f1d1161cf6fdb3 Mon Sep 17 00:00:00 2001 From: KevinGrajeda Date: Wed, 8 Jun 2022 19:16:59 -0500 Subject: [PATCH 009/177] added new test and fixed test --- test/unit/math/trigonometry.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/unit/math/trigonometry.js b/test/unit/math/trigonometry.js index 47d619fb15..3445baa4c3 100644 --- a/test/unit/math/trigonometry.js +++ b/test/unit/math/trigonometry.js @@ -48,17 +48,18 @@ suite('Trigonometry', function() { suite('p5.prototype.angleMode', function() { test('should set constant to DEGREES', function() { myp5.angleMode(DEGREES); - assert.equal(myp5._angleMode, 'degrees'); + assert.equal(myp5.angleMode(), 'degrees'); }); test('should set constant to RADIANS', function() { myp5.angleMode(RADIANS); - assert.equal(myp5._angleMode, 'radians'); + assert.equal(myp5.angleMode(), 'radians'); }); - test('should always be RADIANS or DEGREES', function() { - myp5.angleMode('wtflolzkk'); - assert.equal(myp5._angleMode, 'radians'); + test('wrong param type', function() { + assert.validationError(function() { + myp5.angleMode('wtflolzkk'); + }); }); test('should return radians', function() { @@ -70,6 +71,11 @@ suite('Trigonometry', function() { myp5.angleMode(DEGREES); assert.equal(myp5.angleMode(), 'degrees'); }); + + test('should always be RADIANS or DEGREES', function() { + myp5.angleMode('wtflolzkk'); + assert.equal(myp5.angleMode(), 'radians'); + }); }); suite('p5.prototype.degrees', function() { From aa519c19d076dc292d4853e95400f74b1fea013f Mon Sep 17 00:00:00 2001 From: A M Chung Date: Fri, 10 Jun 2022 10:12:14 -0700 Subject: [PATCH 010/177] added FES survey results; addedi18n book links --- contributor_docs/friendly_error_system.md | 17 +++++++++++++---- translations/ko/README.md | 18 +++++++++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/contributor_docs/friendly_error_system.md b/contributor_docs/friendly_error_system.md index 28652be7b9..f74cbeefda 100644 --- a/contributor_docs/friendly_error_system.md +++ b/contributor_docs/friendly_error_system.md @@ -6,15 +6,24 @@ The Friendly Error System (FES, 🌸) aims to help new programmers by providing The FES prints messages in the console window, as seen in the [p5.js Web Editor] and your browser JavaScript console. The single minified file of p5 (p5.min.js) omits the FES. - *We have an ongoing survey!* Please take a moment to fill out this 5-minute survey to help us improve the FES: [🌸 SURVEY 🌸] - [p5.js Web Editor]: https://editor.p5js.org/ -[🌸 SURVEY 🌸]: https://bit.ly/p5fesSurvey +## Lowering the Barriers to Debugging +The design of a tool should match the need of the people who will use it. As a tool that aims to lower the barriers to debugging, the design of FES is no exception. + +The best way to evaluate our existing design is to hear directly from people using p5.js. We ran a community survey in 2021 to gather feedback and future wishes for Friendly Errors. + +We believe the insights from our community members will be helpful for our contributors. You can see the results through the summary comic or the full report: +* [21-22 FES Survey Report Comic] +* [21-22 FES Survey Full Report] + + +[21-22 FES Survey Report Comic]: https://almchung.github.io/p5jsFESsurvey/ +[21-22 FES Survey Full Report]: https://observablehq.com/@almchung/p5-fes-21-survey ## Writing Friendly Error Messages -In this section, we will describe how you can contribute to the p5.js library by writing and translating error messages. +How to contribute to the p5.js library by writing and translating error messages? The FES is a part of the p5.js' [internationalization] effort. We generate all FES messages' content through [i18next]-based `translator()` function. This dynamic error message generation happens for all languages, including English - the default language of the p5.js. diff --git a/translations/ko/README.md b/translations/ko/README.md index 4603f10aa9..d23fe10d4d 100644 --- a/translations/ko/README.md +++ b/translations/ko/README.md @@ -1,15 +1,27 @@ # Welcome to the FES Korean branch! 안녕하세요, FES 한국어 브랜치에 어서오세요! -## 한국어 Translation Credits +## 한국어 공동 번역 기여자 Korean Translation Credits 2021년 가을부터 공동작업으로 진행되어 2022년 1월에 마무리된 FES 에러메시지 공동 번역 작업은 아래 분들이 함께하셨습니다. * [염인화](https://yinhwa.art/) (Inhwa Yeom): artist/XR researcher based in South Korea. (Take a look at her works on [p5 for 50+](https://p5for50.plus/) ([Processing Foundation Fellows 2020](https://medium.com/processing-foundation/p5-js-for-ages-50-in-korea-50d47b5927fb)) and p5js website Korean translation) * 전유진 (Youjin Jeon): artist/organizer based in Seoul, South Korea. [여성을 위한 열린 기술랩(Woman Open Tech Lab.kr)](http://womanopentechlab.kr/) and [Seoul Express](http://seoulexpress.kr/) * [정앎](https://www.almichu.com/) (Alm Chung, organizer): Korean-American artist/researcher based in Seattle, WA. * 이지현 (Jihyun Lee): Korean publishing editor based in South Korea -## 한국어 Translation Resources -* 추후 추가될 예정입니다! +## 영한 번역 리소스 (Korean-English Translation Resources) +* 영한 [번역에 도움이 되는 툴과 유의점들]입니다. +* 또한 영한 [번역 작업 중 마주치는 딜레마들]속에서 저희가 채택한 방식을 모아 적어봤습니다. +* 외래 [기술 용어 다루기]에 대한 논의입니다. +* p5js.org/ko 기술 색인 입니다. +* 현존하는 검색툴/번역툴들과 연계가능한 "[사이를 맴도는]" 번역문에 대해 생각해보는 글입니다. +이 외에도 FES의 세계화 작업 과정, 그리고 과정 중 논의된 이슈들을 [Friendly Errors i18n Book ✎ 친절한 오류 메시지 세계화 가이드북]에서 읽어보실수 있습니다. + + +[번역에 도움이 되는 툴과 유의점들]: https://almchung.github.io/p5-fes-i18n-book/ch4/#tools +[번역 작업 중 마주치는 딜레마들]: https://almchung.github.io/p5-fes-i18n-book/ch4/#dilemmas +[기술 용어 다루기]: https://almchung.github.io/p5-fes-i18n-book/ch3/ +[사이를 맴도는]: https://almchung.github.io/p5-fes-i18n-book/ch5/ +[Friendly Errors i18n Book ✎ 친절한 오류 메시지 세계화 가이드북]:https://almchung.github.io/p5-fes-i18n-book/ 질문이나 건의 사항은 @almchung 에게 문의주시길 바랍니다. \ No newline at end of file From ee29d396694b81d85e52307cac2fab30d9488000 Mon Sep 17 00:00:00 2001 From: A M Chung Date: Tue, 14 Jun 2022 10:40:57 -0700 Subject: [PATCH 011/177] fes i18n book & writing best practices info updated --- contributor_docs/friendly_error_system.md | 17 ++++++++++------- translations/ko/README.md | 13 ++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/contributor_docs/friendly_error_system.md b/contributor_docs/friendly_error_system.md index f74cbeefda..7cc3ecb2ed 100644 --- a/contributor_docs/friendly_error_system.md +++ b/contributor_docs/friendly_error_system.md @@ -35,21 +35,24 @@ We welcome contributions from all around the world! 🌐 #### Writing Best Practices -FES message writers should prioritize lowering the barrier of understanding error messages and debugging. +FES message writers should prioritize lowering the barrier of understanding error messages and increasing the accessibility of debugging process. -Here are some highlights from our upcoming best-practice doc: +[Friendly Errors i18n Book] discusses challenges and best practices for writing friendly error messages within the cross-cultural i18n context. Here are some points from the book: -* Use simple sentences. Consider breaking your sentence into smaller blocks for best utilizing i18next's [interpolation] feature. -* Keep the language friendly and inclusive. Look for possible bias and harm in your language. Adhere to [p5.js Code of Conduct]. -* Avoid using figures of speech. Prioritize cross-cultural communication. -* Try to spot possible "[expert blind spots]" in an error message and its related docs. -* Introduce one technical concept or term at a time—link one external resource written in a beginner-friendly language with plenty of short, practical examples. +* Understand your audience: do not make assumptions about the audience of our error messages. Try to learn who is using our library and how they use it. +* Keep language inclusive. We strive to make error messages "friendly," what does it mean for you? Look for possible bias and harm in your language. Adhere to [p5.js Code of Conduct]. +* Use simple sentences whenever possible. Consider breaking your sentence into smaller blocks for best utilizing i18next's [interpolation] feature. +* Prioritize cross-cultural communication and provide a great experience across languages. Avoid using figures of speech. +* Introduce one technical concept or technical term at a time. Keep consistency in technical writing. Try to link one external resource written in a beginner-friendly language with plenty of short, practical examples. +[Friendly Errors i18n Book]: https://almchung.github.io/p5-fes-i18n-book/ [interpolation]: https://www.i18next.com/translation-function/interpolation [p5.js Code of Conduct]: https://github.com/processing/p5.js/blob/main/CODE_OF_CONDUCT.md#p5js-code-of-conduct [expert blind spots]: https://tilt.colostate.edu/TipsAndGuides/Tip/181 +[Friendly Errors i18n Book] is a public project, and you can contribute to the book through this separate [repo]. +[repo]: https://github.com/almchung/p5-fes-i18n-book #### Location of Translation Files `translator()` is based on i18next and imported from `src/core/internationalization.js`. It generates messages by looking up text data from a JSON translation file: diff --git a/translations/ko/README.md b/translations/ko/README.md index d23fe10d4d..c15844116f 100644 --- a/translations/ko/README.md +++ b/translations/ko/README.md @@ -10,18 +10,17 @@ ## 영한 번역 리소스 (Korean-English Translation Resources) * 영한 [번역에 도움이 되는 툴과 유의점들]입니다. -* 또한 영한 [번역 작업 중 마주치는 딜레마들]속에서 저희가 채택한 방식을 모아 적어봤습니다. +* 또한 영한 [번역 작업 중 마주치는 딜레마들] 속에서 저희가 채택한 방식을 모아 적어봤습니다. * 외래 [기술 용어 다루기]에 대한 논의입니다. -* p5js.org/ko 기술 색인 입니다. -* 현존하는 검색툴/번역툴들과 연계가능한 "[사이를 맴도는]" 번역문에 대해 생각해보는 글입니다. +* p5js 웹사이트와 기술 문서에서 사용하는 기술 용어들을 통일하기위해 사용하고 있 [p5js.org/ko 기술 용어 색인] 입니다. +* 현존하는 검색툴/번역 툴들과 연계 가능한 "[사이를 맴도는]" 번역문에 대해 생각해보는 글입니다. -이 외에도 FES의 세계화 작업 과정, 그리고 과정 중 논의된 이슈들을 [Friendly Errors i18n Book ✎ 친절한 오류 메시지 세계화 가이드북]에서 읽어보실수 있습니다. +이 외에도 FES의 세계화 작업 과정, 그리고 과정 중 논의된 이슈들을 [Friendly Errors i18n Book ✎ 친절한 오류 메시지 세계화 가이드북]에서 읽어보실 수 있습니다. "친절한 오류 메시지 세계화 가이드북"은 오픈 소스 프로젝트이며, 이 [독립된 레파지토리 (repository)]를 통해 기여 가능합니다. [번역에 도움이 되는 툴과 유의점들]: https://almchung.github.io/p5-fes-i18n-book/ch4/#tools [번역 작업 중 마주치는 딜레마들]: https://almchung.github.io/p5-fes-i18n-book/ch4/#dilemmas [기술 용어 다루기]: https://almchung.github.io/p5-fes-i18n-book/ch3/ [사이를 맴도는]: https://almchung.github.io/p5-fes-i18n-book/ch5/ -[Friendly Errors i18n Book ✎ 친절한 오류 메시지 세계화 가이드북]:https://almchung.github.io/p5-fes-i18n-book/ - -질문이나 건의 사항은 @almchung 에게 문의주시길 바랍니다. \ No newline at end of file +[Friendly Errors i18n Book ✎ 친절한 오류 메시지 세계화 가이드북]: https://almchung.github.io/p5-fes-i18n-book/ +[독립된 레파지토리 (repository)]: https://github.com/almchung/p5-fes-i18n-book \ No newline at end of file From 21f5e3db573cad9ef4a00821a5468b31549656a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 19 Jun 2022 11:43:05 +0200 Subject: [PATCH 012/177] update documentation --- src/image/image.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index 8260776c13..c38a645c8f 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -420,15 +420,20 @@ p5.prototype.saveGif = function(pImg, filename) { * as an argument to the callback function as an array of objects, with the * size of array equal to the total number of frames. * - * Note that saveFrames() will only save the first 15 frames of an animation. + * Note that saveFrames() will only save the first 15 seconds of an animation. + * The arguments `duration` and `framerate` are constrained to be less or equal 15 and 22, respectively, which means you + * can only download a maximum of 15 seconds worth of frames at 22 frames per second, adding up to 330 frames. + * This is done in order to avoid memory problems since a large enough canvas can fill up the memory in your computer + * very easily and crash your program or even your browser. + * * To export longer animations, you might look into a library like * ccapture.js. * * @method saveFrames * @param {String} filename * @param {String} extension 'jpg' or 'png' - * @param {Number} duration Duration in seconds to save the frames for. - * @param {Number} framerate Framerate to save the frames in. + * @param {Number} duration Duration in seconds to save the frames for. This parameter will be constrained to be less or equal to 15. + * @param {Number} framerate Framerate to save the frames in. This parameter will be constrained to be less or equal to 22. * @param {function(Array)} [callback] A callback function that will be executed to handle the image data. This function should accept an array as argument. The From a0a151a6b51a0bdd7ddd4599659a602b24e8e9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 19 Jun 2022 11:43:30 +0200 Subject: [PATCH 013/177] fix linter error --- src/image/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image/image.js b/src/image/image.js index c38a645c8f..0b329f01cb 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -69,7 +69,7 @@ import omggif from 'omggif'; * let img = createImage(66, 66); * img.loadPixels(); * let d = pixelDensity(); - * let halfImage = 4 * (img.width * d) * (img.height / 2 * d); + * let halfImage = 4 * (img.width * d) * ((img.height / 2) * d); * for (let i = 0; i < halfImage; i += 4) { * img.pixels[i] = red(pink); * img.pixels[i + 1] = green(pink); From 8081481ddbd26f5967293ee9d1244da0b587deba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 01:18:02 +0000 Subject: [PATCH 014/177] build(deps): bump shell-quote from 1.7.2 to 1.7.3 Bumps [shell-quote](https://github.com/substack/node-shell-quote) from 1.7.2 to 1.7.3. - [Release notes](https://github.com/substack/node-shell-quote/releases) - [Changelog](https://github.com/substack/node-shell-quote/blob/master/CHANGELOG.md) - [Commits](https://github.com/substack/node-shell-quote/compare/v1.7.2...1.7.3) --- updated-dependencies: - dependency-name: shell-quote dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fbf0059a65..ffff484561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14105,9 +14105,9 @@ "dev": true }, "shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "dev": true }, "signal-exit": { From 16904a8c3f121461478e71bb540914d17c8a8d6d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 14:10:07 +0000 Subject: [PATCH 015/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a2b4ce7670..e0c6489ed1 100644 --- a/README.md +++ b/README.md @@ -562,6 +562,7 @@ We recognize all types of contributions. This project follows the [all-contribut
smilee

💻
CommanderRoot

💻
Philip Bell

📖 +
tapioca24

🔌 From a72eec29803db7f10fbbfad667f2dd347ddca462 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 14:10:08 +0000 Subject: [PATCH 016/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 514d88aace..9456faf3b9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3059,6 +3059,15 @@ "contributions": [ "doc" ] + }, + { + "login": "tapioca24", + "name": "tapioca24", + "avatar_url": "https://avatars.githubusercontent.com/u/12683107?v=4", + "profile": "https://github.com/tapioca24", + "contributions": [ + "plugin" + ] } ], "repoType": "github", From 6f43aa8a01a708dd4b6a4ed2455ab275b78df5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 1 Jul 2022 19:03:47 +0200 Subject: [PATCH 017/177] initial commits! --- lib/empty-example/sketch.js | 18 +++++++- src/image/image.js | 89 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index de6c862644..fe73fb1306 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,7 +1,23 @@ +/* eslint-disable no-unused-vars */ + function setup() { // put setup code here + createCanvas(600, 600); + frameRate(3); } function draw() { // put drawing code here -} \ No newline at end of file + background(20); + // print(frameRate()); + + circle( + 100 * sin(frameCount / 10) + width / 2, + 100 * sin(frameCount / 10) + height / 2, + 10 + ); +} + +function mousePressed() { + createGif('mySketch', 2); +} diff --git a/src/image/image.js b/src/image/image.js index 8260776c13..8bb8394412 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -184,8 +184,97 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; +p5.prototype.createGif = function(...args) { + // process args + + let fileName; + let seconds; + let delay; + // let callback; + + switch (args.length) { + case 2: + fileName = args[0]; + seconds = args[1]; + break; + case 3: + fileName = args[0]; + seconds = args[1]; + delay = args[2]; + break; + default: + fileName = args[0]; + seconds = args[1]; + delay = args[2]; + callback = args[3]; + } + + const makeFrame = p5.prototype._makeFrame; + const cnv = this._curElement.elt; + + let ext = 'png'; + + if (!delay) { + delay = 0; + } + print(fileName, seconds, delay); + + let frameRate = this._frameRate || this._targetFrameRate || 60; + let nFrames = seconds * frameRate; + let nFramesDelay = delay * frameRate; + + // TODO: check if delay and nFrames are correct + /* + Natural behaviour for me is wait for nDelay seconds + and then process the next nFrame seconds. Right now this + waits nDelay seconds but processess nFrames - nDelay seconds. + */ + var count = nFramesDelay; + this.frameCount = count; + // var pImg = new p5.Image(this.width, this.height); + var frameBuffer = []; + noLoop(); + + while (count < nFrames) { + /* we draw the next frame. this is important, since + busy sketches or low end devices might take longer + to render the frame. So we just wait for the frame + to be drawn and immediately save it to a buffer and continue + */ + redraw(); + frameBuffer.push(makeFrame(fileName + count, ext, cnv)); + count++; + console.log(frameCount); + console.log('Processing frame ' + count); + } + print(frameBuffer); + _createGif(); + + // if (callback) { + // callback(frameBuffer); + // } else { + // for (const f of frameBuffer) { + // print(f.imageData, f.filename, f.ext); + // // p5.prototype.downloadFile(f.imageData, f.filename, f.ext); + // } + // } + + // const extension = 'gif'; + // const blob = new Blob([frameBuffer], { + // type: 'image/gif', + // }); + // let pImg = new p5.Image(frameBuffer); + // print(pImg); + // let gif = p5.prototype.saveGif(blob, fileName); + // print(gif); + + print('No loop'); + noLoop(); +}; + p5.prototype.saveGif = function(pImg, filename) { const props = pImg.gifProperties; + console.log(props); //convert loopLimit back into Netscape Block formatting let loopLimit = props.loopLimit; From 1a9ae6b874e43023183cc8539046055cc062b64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 1 Jul 2022 19:07:11 +0200 Subject: [PATCH 018/177] fix linter error --- src/image/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image/image.js b/src/image/image.js index 0b329f01cb..c38a645c8f 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -69,7 +69,7 @@ import omggif from 'omggif'; * let img = createImage(66, 66); * img.loadPixels(); * let d = pixelDensity(); - * let halfImage = 4 * (img.width * d) * ((img.height / 2) * d); + * let halfImage = 4 * (img.width * d) * (img.height / 2 * d); * for (let i = 0; i < halfImage; i += 4) { * img.pixels[i] = red(pink); * img.pixels[i + 1] = green(pink); From 980811887a4b8bc0536f795d6966a1d37854e755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 1 Jul 2022 22:41:11 +0200 Subject: [PATCH 019/177] remove redundant information and fix typo --- src/image/image.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index c38a645c8f..8ac3a24bba 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -420,8 +420,7 @@ p5.prototype.saveGif = function(pImg, filename) { * as an argument to the callback function as an array of objects, with the * size of array equal to the total number of frames. * - * Note that saveFrames() will only save the first 15 seconds of an animation. - * The arguments `duration` and `framerate` are constrained to be less or equal 15 and 22, respectively, which means you + * The arguments `duration` and `framerate` are constrained to be less or equal to 15 and 22, respectively, which means you * can only download a maximum of 15 seconds worth of frames at 22 frames per second, adding up to 330 frames. * This is done in order to avoid memory problems since a large enough canvas can fill up the memory in your computer * very easily and crash your program or even your browser. From d86e4ac062b59a04ccf4b1d6575f2e7d8432b9a0 Mon Sep 17 00:00:00 2001 From: Austin Slominski Date: Fri, 1 Jul 2022 16:07:10 -0600 Subject: [PATCH 020/177] added describe() to all 3d reference examples, resolving issue #5705 --- src/webgl/3d_primitives.js | 54 ++++++++++++++++++++++++++++++++++++++ src/webgl/interaction.js | 20 ++++++++++++++ src/webgl/light.js | 22 ++++++++++++++++ src/webgl/loading.js | 6 +++++ src/webgl/material.js | 35 ++++++++++++++++++++++++ src/webgl/p5.Camera.js | 42 +++++++++++++++++++++++++++++ src/webgl/p5.Shader.js | 4 +++ 7 files changed, 183 insertions(+) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 090a89479e..779af9491c 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -33,6 +33,8 @@ import * as constants from '../core/constants'; * background(200); * plane(50, 50); * } + * + * describe('a white plane with black wireframe lines'); *
* * @@ -120,6 +122,8 @@ p5.prototype.plane = function(width, height, detailX, detailY) { * rotateY(frameCount * 0.01); * box(50); * } + * + * describe('a white box rotating in 3D space'); * * */ @@ -237,6 +241,8 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * background(205, 102, 94); * sphere(40); * } + * + * describe('a white sphere with black wireframe lines'); * * * @@ -257,6 +263,10 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * rotateY(millis() / 1000); * sphere(40, detailX.value(), 16); * } + * + * describe( + * 'a white sphere with low detail on the x-axis, including a slider to adjust detailX' + * ); * * * @@ -277,6 +287,10 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * rotateY(millis() / 1000); * sphere(40, 16, detailY.value()); * } + * + * describe( + * 'a white sphere with low detail on the y-axis, including a slider to adjust detailY' + * ); * * */ @@ -449,6 +463,8 @@ const _truncatedCone = function( * rotateZ(frameCount * 0.01); * cylinder(20, 50); * } + * + * describe('a rotating white cylinder'); * * * @@ -469,6 +485,10 @@ const _truncatedCone = function( * rotateY(millis() / 1000); * cylinder(20, 75, detailX.value(), 1); * } + * + * describe( + * 'a rotating white cylinder with limited X detail, with a slider that adjusts detailX' + * ); * * * @@ -489,6 +509,10 @@ const _truncatedCone = function( * rotateY(millis() / 1000); * cylinder(20, 75, 16, detailY.value()); * } + * + * describe( + * 'a rotating white cylinder with limited Y detail, with a slider that adjusts detailY' + * ); * * */ @@ -584,6 +608,8 @@ p5.prototype.cylinder = function( * rotateZ(frameCount * 0.01); * cone(40, 70); * } + * + * describe('a rotating white cone'); * * * @@ -604,6 +630,10 @@ p5.prototype.cylinder = function( * rotateY(millis() / 1000); * cone(30, 65, detailX.value(), 16); * } + * + * describe( + * 'a rotating white cone with limited X detail, with a slider that adjusts detailX' + * ); * * * @@ -624,6 +654,10 @@ p5.prototype.cylinder = function( * rotateY(millis() / 1000); * cone(30, 65, 16, detailY.value()); * } + * + * describe( + * 'a rotating white cone with limited Y detail, with a slider that adjusts detailY' + * ); * * */ @@ -698,6 +732,8 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * background(205, 105, 94); * ellipsoid(30, 40, 40); * } + * + * describe('a white 3d ellipsoid'); * * * @@ -718,6 +754,10 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * rotateY(millis() / 1000); * ellipsoid(30, 40, 40, detailX.value(), 8); * } + * + * describe( + * 'a rotating white ellipsoid with limited X detail, with a slider that adjusts detailX' + * ); * * * @@ -738,6 +778,10 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * rotateY(millis() / 1000); * ellipsoid(30, 40, 40, 12, detailY.value()); * } + * + * describe( + * 'a rotating white ellipsoid with limited Y detail, with a slider that adjusts detailY' + * ); * * */ @@ -834,6 +878,8 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * rotateY(frameCount * 0.01); * torus(30, 15); * } + * + * describe('a rotating white torus'); * * * @@ -854,6 +900,10 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * rotateY(millis() / 1000); * torus(30, 15, detailX.value(), 12); * } + * + * describe( + * 'a rotating white torus with limited X detail, with a slider that adjusts detailX' + * ); * * * @@ -874,6 +924,10 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * rotateY(millis() / 1000); * torus(30, 15, 16, detailY.value()); * } + * + * describe( + * 'a rotating white torus with limited Y detail, with a slider that adjusts detailY' + * ); * * */ diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index 87680921df..26d8a32493 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -37,6 +37,7 @@ import * as constants from '../core/constants'; * rotateY(0.5); * box(30, 50); * } + * describe('Camera orbits around a box when mouse is hold-clicked & then moved.'); * * * @@ -178,6 +179,9 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * noDebugMode(); * } * } + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' + * ); * * * @alt @@ -201,6 +205,8 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * orbitControl(); * box(15, 30); * } + * + * describe('a 3D box is centered on a grid in a 3D sketch.'); * * * @alt @@ -221,6 +227,10 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * orbitControl(); * box(15, 30); * } + * + * describe( + * 'a 3D box is centered in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' + * ); * * * @alt @@ -243,6 +253,8 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * orbitControl(); * box(15, 30); * } + * + * describe('a 3D box is centered on a grid in a 3D sketch'); * * * @alt @@ -267,6 +279,10 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * stroke(255, 0, 150); * strokeWeight(0.8); * } + * + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' + * ); * * * @alt @@ -372,6 +388,10 @@ p5.prototype.debugMode = function(...args) { * noDebugMode(); * } * } + * + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' + * ); * * * @alt diff --git a/src/webgl/light.js b/src/webgl/light.js index 559f3a468e..010df053e5 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -44,6 +44,7 @@ import * as constants from '../core/constants'; * ambientMaterial(255, 127, 80); // coral material * sphere(40); * } + * describe('sphere with coral color under black light'); * * * @alt @@ -62,6 +63,7 @@ import * as constants from '../core/constants'; * ambientMaterial(255, 127, 80); // coral material * sphere(40); * } + * describe('sphere with coral color under white light'); * * * @alt @@ -172,6 +174,10 @@ p5.prototype.ambientLight = function(v1, v2, v3, a) { * function mouseClicked() { * setRedSpecularColor = !setRedSpecularColor; * } + * + * describe( + * 'Sphere with specular highlight. Clicking the mouse toggles the specular highlight color between red and the default white.' + * ); * * * @@ -266,6 +272,9 @@ p5.prototype.specularColor = function(v1, v2, v3) { * noStroke(); * sphere(40); * } + * describe( + * 'scene with sphere and directional light. The direction of the light is controlled with the mouse position.' + * ); * * * @@ -391,6 +400,9 @@ p5.prototype.directionalLight = function(v1, v2, v3, x, y, z) { * noStroke(); * sphere(40); * } + * describe( + * 'scene with sphere and point light. The position of the light is controlled with the mouse position.' + * ); * * * @@ -490,6 +502,7 @@ p5.prototype.pointLight = function(v1, v2, v3, x, y, z) { * rotateZ(millis() / 1000); * box(); * } + * describe('the light is partially ambient and partially directional'); * * * @@ -557,6 +570,9 @@ p5.prototype.lights = function() { * sphere(20); * pop(); * } + * describe( + * 'Two spheres with different falloff values show different intensity of light' + * ); * * * @@ -672,6 +688,9 @@ p5.prototype.lightFalloff = function( * noStroke(); * sphere(40); * } + * describe( + * 'scene with sphere and spot light. The position of the light is controlled with the mouse position.' + * ); * * * @@ -1005,6 +1024,9 @@ p5.prototype.spotLight = function( * ambientMaterial(255); * sphere(13); * } + * describe( + * 'Three white spheres. Each appears as a different color due to lighting.' + * ); * * * diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 78e19145c2..39ed1d9b0e 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -58,6 +58,8 @@ import './p5.Geometry'; * rotateY(frameCount * 0.01); * model(octahedron); * } + * + * describe('Vertically rotating 3-d octahedron.'); * * * @@ -87,6 +89,8 @@ import './p5.Geometry'; * normalMaterial(); // For effect * model(teapot); * } + * + * describe('Vertically rotating 3-d teapot with red, green and blue gradient.'); * * * @@ -610,6 +614,8 @@ function parseASCIISTL(model, lines) { * rotateY(frameCount * 0.01); * model(octahedron); * } + * + * describe('Vertically rotating 3-d octahedron.'); * * * diff --git a/src/webgl/material.js b/src/webgl/material.js index 075d081674..89078ea821 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -51,6 +51,8 @@ import './p5.Texture'; * mandel.setUniform('r', 1.5 * exp(-6.5 * (1 + sin(millis() / 2000)))); * quad(-1, -1, 1, -1, 1, 1, -1, 1); * } + * + * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * * * @@ -169,6 +171,8 @@ p5.prototype.loadShader = function( * mandel.setUniform('r', 1.5 * exp(-6.5 * (1 + sin(millis() / 2000)))); * quad(-1, -1, 1, -1, 1, 1, -1, 1); * } + * + * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * * * @@ -253,6 +257,10 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * function mouseClicked() { * showRedGreen = !showRedGreen; * } + * + * describe( + * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' + * ); * * * @@ -354,6 +362,10 @@ p5.prototype.shader = function(s) { * box(width / 4); * pop(); * } + * + * describe( + * 'Two rotating cubes. The left one is painted using a custom (user-defined) shader, while the right one is painted using the default fill shader.' + * ); * * * @alt @@ -403,6 +415,8 @@ p5.prototype.resetShader = function() { * texture(img); * box(width / 2); * } + * + * describe('spinning cube with a texture from an image'); * * * @alt @@ -429,6 +443,8 @@ p5.prototype.resetShader = function() { * noStroke(); * plane(50); * } + * + * describe('plane with a texture from an image created by createGraphics()'); * * * @alt @@ -456,6 +472,8 @@ p5.prototype.resetShader = function() { * function mousePressed() { * vid.loop(); * } + * + * describe('rectangle with video as texture'); * * * @@ -486,6 +504,8 @@ p5.prototype.resetShader = function() { * vertex(-40, 40, 0, 1); * endShape(); * } + * + * describe('quad with a texture, mapped using normalized coordinates'); * * * @alt @@ -541,6 +561,8 @@ p5.prototype.texture = function(tex) { * vertex(-50, 50, 0, 1); * endShape(); * } + * + * describe('quad with a texture, mapped using normalized coordinates'); * * * @alt @@ -569,6 +591,8 @@ p5.prototype.texture = function(tex) { * vertex(-50, 50, 0, img.height); * endShape(); * } + * + * describe('quad with a texture, mapped using image coordinates'); * * * @alt @@ -640,6 +664,8 @@ p5.prototype.textureMode = function(mode) { * vertex(-1, -1, 0, 0, 0); * endShape(); * } + * + * describe('an image of the rocky mountains repeated in mirrored tiles'); * * * @@ -682,6 +708,8 @@ p5.prototype.textureWrap = function(wrapX, wrapY = wrapX) { * normalMaterial(); * sphere(40); * } + * + * describe('Sphere with normal material'); * * * @alt @@ -739,6 +767,7 @@ p5.prototype.normalMaterial = function(...args) { * ambientMaterial(70, 130, 230); * sphere(40); * } + * describe('sphere reflecting red, blue, and green light'); * * * @alt @@ -758,6 +787,7 @@ p5.prototype.normalMaterial = function(...args) { * ambientMaterial(255); // white material * box(30); * } + * describe('box reflecting only red and blue light'); * * * @alt @@ -777,6 +807,7 @@ p5.prototype.normalMaterial = function(...args) { * ambientMaterial(255, 0, 255); // magenta material * box(30); * } + * describe('box reflecting no light'); * * * @alt @@ -849,6 +880,7 @@ p5.prototype.ambientMaterial = function(v1, v2, v3) { * emissiveMaterial(130, 230, 0); * sphere(40); * } + * describe('sphere with green emissive material'); * * * @@ -932,6 +964,8 @@ p5.prototype.emissiveMaterial = function(v1, v2, v3, a) { * shininess(50); * torus(30, 10, 64, 64); * } + * + * describe('torus with specular material'); * * * @alt @@ -1002,6 +1036,7 @@ p5.prototype.specularMaterial = function(v1, v2, v3, alpha) { * shininess(20); * sphere(20); * } + * describe('two spheres, one more shiny than the other'); * * * @alt diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index f09dee3923..03c3b0e91b 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -52,6 +52,7 @@ import p5 from '../core/main'; * camera(0, 0, 20 + sin(frameCount * 0.01) * 10, 0, 0, 0, 0, 1, 0); * plane(10, 10); * } + * describe('a square moving closer and then away from the camera.'); * * * @@ -98,6 +99,10 @@ import p5 from '../core/main'; * fill(255, 102, 94); * box(85); * } + * + * describe( + * 'White square repeatedly grows to fill canvas and then shrinks. An interactive example of a red cube with 3 sliders for moving it across x, y, z axis and 3 sliders for shifting its center.' + * ); * * * @alt @@ -160,6 +165,9 @@ p5.prototype.camera = function(...args) { * box(30); * pop(); * } + * describe( + * 'two colored 3D boxes move back and forth, rotating as mouse is dragged.' + * ); * * * @@ -220,6 +228,9 @@ p5.prototype.perspective = function(...args) { * box(30); * pop(); * } + * describe( + * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' + * ); * * * @@ -283,6 +294,9 @@ p5.prototype.ortho = function(...args) { * box(30); * pop(); * } + * describe( + * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' + * ); * * * @@ -335,6 +349,8 @@ p5.prototype.frustum = function(...args) { * camera.setPosition(sin(frameCount / 60) * 200, 0, 100); * box(20); * } + * + * describe('An example that creates a camera and moves it around the box.'); * * * @alt @@ -427,6 +443,10 @@ p5.prototype.createCamera = function() { * translate(35, 0, 0); * box(20); * } + * + * describe( + * 'camera view pans left and right across a series of rotating 3D boxes.' + * ); * * * @@ -462,6 +482,8 @@ p5.Camera = function(renderer) { * box(10); * div.html('eyeX = ' + cam.eyeX); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -489,6 +511,8 @@ p5.Camera = function(renderer) { * box(10); * div.html('eyeY = ' + cam.eyeY); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -516,6 +540,8 @@ p5.Camera = function(renderer) { * box(10); * div.html('eyeZ = ' + cam.eyeZ); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -544,6 +570,8 @@ p5.Camera = function(renderer) { * orbitControl(); * box(10); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -572,6 +600,8 @@ p5.Camera = function(renderer) { * orbitControl(); * box(10); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -600,6 +630,8 @@ p5.Camera = function(renderer) { * orbitControl(); * box(10); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -623,6 +655,8 @@ p5.Camera = function(renderer) { * div.style('color', 'blue'); * div.style('font-size', '18px'); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -646,6 +680,8 @@ p5.Camera = function(renderer) { * div.style('color', 'blue'); * div.style('font-size', '18px'); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -669,6 +705,8 @@ p5.Camera = function(renderer) { * div.style('color', 'blue'); * div.style('font-size', '18px'); * } + * + * describe('An example showing the use of camera object properties'); * * * @alt @@ -1784,6 +1822,10 @@ p5.Camera.prototype._isActive = function() { * translate(35, 0, 0); * box(20); * } + * + * describe( + * 'Canvas switches between two camera views, each showing a series of spinning 3D boxes.' + * ); * * * diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 740e95978e..a4014b31af 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -363,6 +363,10 @@ p5.Shader.prototype.useProgram = function() { * function mouseClicked() { * showRedGreen = !showRedGreen; * } + * + * describe( + * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' + * ); * * * From dd194ad190b6061ba667cd66db8a7aac177dfc3f Mon Sep 17 00:00:00 2001 From: evelyn masso Date: Fri, 1 Jul 2022 16:11:32 -0700 Subject: [PATCH 021/177] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0c6489ed1..16f75510d5 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Stewards are contributors that are particularly involved, familiar, or responsiv Anyone interested can volunteer to be a steward! There are no specific requirements for expertise, just an interest in actively learning and participating. If you’re familiar with one or more parts of this project, open an issue to volunteer as a steward! -* [@outofambit](https://github.com/outofambit) - project co-lead * [@qianqianye](https://github.com/qianqianye) - project co-lead +* [@outofambit](https://github.com/outofambit) * [@lmccart](https://github.com/lmccart) * [@limzykenneth](https://github.com/limzykenneth) * [@stalgiag](https://github.com/stalgiag) From e005d801993e04f7cf7cbf37046bfb11c7daae38 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 3 Jul 2022 04:52:27 +0000 Subject: [PATCH 022/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e0c6489ed1..ae83abd20d 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,7 @@ We recognize all types of contributions. This project follows the [all-contribut
CommanderRoot

💻
Philip Bell

📖
tapioca24

🔌 +
Qianqian Ye

💻 🎨 📖 📋 👀 🌍 From a474626c219de6f93dafc148e47a6b3ef08b4270 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 3 Jul 2022 04:52:28 +0000 Subject: [PATCH 023/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9456faf3b9..7efcd834f1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3068,6 +3068,20 @@ "contributions": [ "plugin" ] + }, + { + "login": "Qianqianye", + "name": "Qianqian Ye", + "avatar_url": "https://avatars.githubusercontent.com/u/18587130?v=4", + "profile": "http://qianqian-ye.com", + "contributions": [ + "code", + "design", + "doc", + "eventOrganizing", + "review", + "translation" + ] } ], "repoType": "github", From b19d572dd1442c5145c4d8463374a5c0b44e37cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 3 Jul 2022 13:39:28 +0200 Subject: [PATCH 024/177] proper behaviour for delay and seconds --- lib/empty-example/sketch.js | 2 +- src/image/image.js | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index fe73fb1306..76757e7537 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -19,5 +19,5 @@ function draw() { } function mousePressed() { - createGif('mySketch', 2); + createGif('mySketch', 2, 2); } diff --git a/src/image/image.js b/src/image/image.js index 8bb8394412..0611243011 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -217,11 +217,11 @@ p5.prototype.createGif = function(...args) { if (!delay) { delay = 0; } - print(fileName, seconds, delay); let frameRate = this._frameRate || this._targetFrameRate || 60; - let nFrames = seconds * frameRate; - let nFramesDelay = delay * frameRate; + let nFrames = ceil(seconds * frameRate); + let nFramesDelay = ceil(delay * frameRate); + print(frameRate, nFrames, nFramesDelay); // TODO: check if delay and nFrames are correct /* @@ -235,7 +235,7 @@ p5.prototype.createGif = function(...args) { var frameBuffer = []; noLoop(); - while (count < nFrames) { + while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since busy sketches or low end devices might take longer to render the frame. So we just wait for the frame @@ -244,11 +244,9 @@ p5.prototype.createGif = function(...args) { redraw(); frameBuffer.push(makeFrame(fileName + count, ext, cnv)); count++; - console.log(frameCount); - console.log('Processing frame ' + count); } print(frameBuffer); - _createGif(); + // _createGif(); // if (callback) { // callback(frameBuffer); @@ -261,12 +259,12 @@ p5.prototype.createGif = function(...args) { // const extension = 'gif'; // const blob = new Blob([frameBuffer], { - // type: 'image/gif', + // type: 'image/gif' // }); - // let pImg = new p5.Image(frameBuffer); - // print(pImg); - // let gif = p5.prototype.saveGif(blob, fileName); - // print(gif); + let pImg = new p5.Image(frameBuffer); + print(pImg); + let gif = p5.prototype.saveGif(blob, fileName); + print(gif); print('No loop'); noLoop(); From 228ae550e4e7364732360eef4459138d54042a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 3 Jul 2022 16:00:42 +0200 Subject: [PATCH 025/177] change function from image.js to loading_displaying.js --- src/image/image.js | 87 ------------------------------- src/image/loading_displaying.js | 91 +++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 87 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index 0611243011..8260776c13 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -184,95 +184,8 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; -p5.prototype.createGif = function(...args) { - // process args - - let fileName; - let seconds; - let delay; - // let callback; - - switch (args.length) { - case 2: - fileName = args[0]; - seconds = args[1]; - break; - case 3: - fileName = args[0]; - seconds = args[1]; - delay = args[2]; - break; - default: - fileName = args[0]; - seconds = args[1]; - delay = args[2]; - callback = args[3]; - } - - const makeFrame = p5.prototype._makeFrame; - const cnv = this._curElement.elt; - - let ext = 'png'; - - if (!delay) { - delay = 0; - } - - let frameRate = this._frameRate || this._targetFrameRate || 60; - let nFrames = ceil(seconds * frameRate); - let nFramesDelay = ceil(delay * frameRate); - print(frameRate, nFrames, nFramesDelay); - - // TODO: check if delay and nFrames are correct - /* - Natural behaviour for me is wait for nDelay seconds - and then process the next nFrame seconds. Right now this - waits nDelay seconds but processess nFrames - nDelay seconds. - */ - var count = nFramesDelay; - this.frameCount = count; - // var pImg = new p5.Image(this.width, this.height); - var frameBuffer = []; - noLoop(); - - while (count < nFrames + nFramesDelay) { - /* we draw the next frame. this is important, since - busy sketches or low end devices might take longer - to render the frame. So we just wait for the frame - to be drawn and immediately save it to a buffer and continue - */ - redraw(); - frameBuffer.push(makeFrame(fileName + count, ext, cnv)); - count++; - } - print(frameBuffer); - // _createGif(); - - // if (callback) { - // callback(frameBuffer); - // } else { - // for (const f of frameBuffer) { - // print(f.imageData, f.filename, f.ext); - // // p5.prototype.downloadFile(f.imageData, f.filename, f.ext); - // } - // } - - // const extension = 'gif'; - // const blob = new Blob([frameBuffer], { - // type: 'image/gif' - // }); - let pImg = new p5.Image(frameBuffer); - print(pImg); - let gif = p5.prototype.saveGif(blob, fileName); - print(gif); - - print('No loop'); - noLoop(); -}; - p5.prototype.saveGif = function(pImg, filename) { const props = pImg.gifProperties; - console.log(props); //convert loopLimit back into Netscape Block formatting let loopLimit = props.loopLimit; diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 3ff2b3f053..7c374b644d 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -159,6 +159,97 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { return pImg; }; +p5.prototype.createGif = function(...args) { + // process args + + let fileName; + let seconds; + let delay; + // let callback; + + switch (args.length) { + case 2: + fileName = args[0]; + seconds = args[1]; + break; + case 3: + fileName = args[0]; + seconds = args[1]; + delay = args[2]; + break; + default: + fileName = args[0]; + seconds = args[1]; + delay = args[2]; + callback = args[3]; + } + + // const makeFrame = p5.prototype._makeFrame; + // const cnv = this._curElement.elt; + + // let ext = 'png'; + + if (!delay) { + delay = 0; + } + + let frameRate = this._frameRate || this._targetFrameRate || 60; + let nFrames = ceil(seconds * frameRate); + let nFramesDelay = ceil(delay * frameRate); + print(frameRate, nFrames, nFramesDelay); + + var count = nFramesDelay; + this.frameCount = count; + + // var frameBuffer = new Uint8Array(this.width * this.height * 4 * nFrames); + var pImg = new p5.Image(this.width, this.height, this); + + noLoop(); + + // var loopLimit = 0; //loops forever + // pImg.gifProperties = { + // displayIndex: 0, + // loopLimit, + // loopCount: 0, + // nFrames, + // playing: true, + // timeDisplayed: 0, + // lastChangeTime: 0 + // }; + + while (count < nFrames + nFramesDelay) { + /* + we draw the next frame. this is important, since + busy sketches or low end devices might take longer + to render the frame. So we just wait for the frame + to be drawn and immediately save it to a buffer and continue + */ + redraw(); + // frameBuffer.push(makeFrame(fileName + count, ext, cnv)); + // frameBuffer; + let framePixels = this.drawingContext.getImageData( + 0, + 0, + this.width, + this.height + ).data; + print(framePixels); + const imageData = new ImageData(framePixels, pImg.width, pImg.height); + pImg.drawingContext.putImageData(imageData, 0, 0); + + count++; + print('Processing frame: ' + count); + print('Frame count: ' + this.frameCount); + } + + print(pImg); + saveGif(pImg, fileName); + + print('No loop'); + noLoop(); + frameBuffer = []; +}; + /** * Helper function for loading GIF-based images */ From 98be55f3ac4487c28452957c8a8e93a70a409bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 3 Jul 2022 17:04:54 +0200 Subject: [PATCH 026/177] change naming scheme --- lib/empty-example/sketch.js | 2 +- src/image/image.js | 2 +- src/image/loading_displaying.js | 66 +++++++++++++++++---------------- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 76757e7537..dc942874b9 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -19,5 +19,5 @@ function draw() { } function mousePressed() { - createGif('mySketch', 2, 2); + saveGif('mySketch', 2, 2); } diff --git a/src/image/image.js b/src/image/image.js index 8260776c13..6a3e9fa410 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -184,7 +184,7 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; -p5.prototype.saveGif = function(pImg, filename) { +p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const props = pImg.gifProperties; //convert loopLimit back into Netscape Block formatting diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 7c374b644d..3ae982b390 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -159,8 +159,9 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { return pImg; }; -p5.prototype.createGif = function(...args) { +p5.prototype.saveGif = function(...args) { // process args + // let fileName; let seconds; @@ -184,11 +185,6 @@ p5.prototype.createGif = function(...args) { callback = args[3]; } - // const makeFrame = p5.prototype._makeFrame; - // const cnv = this._curElement.elt; - - // let ext = 'png'; - if (!delay) { delay = 0; } @@ -201,22 +197,15 @@ p5.prototype.createGif = function(...args) { var count = nFramesDelay; this.frameCount = count; - // var frameBuffer = new Uint8Array(this.width * this.height * 4 * nFrames); - var pImg = new p5.Image(this.width, this.height, this); + // width * height * (r,g,b,a) * frames + // var frameBuffer = new Uint8ClampedArray( + // this.width * this.height * 4 * nFrames + // ); + let frames = []; + let pImg = new p5.Image(this.width, this.height, this); noLoop(); - // var loopLimit = 0; //loops forever - // pImg.gifProperties = { - // displayIndex: 0, - // loopLimit, - // loopCount: 0, - // nFrames, - // playing: true, - // timeDisplayed: 0, - // lastChangeTime: 0 - // }; - while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -225,29 +214,42 @@ p5.prototype.createGif = function(...args) { to be drawn and immediately save it to a buffer and continue */ redraw(); - // frameBuffer.push(makeFrame(fileName + count, ext, cnv)); - // frameBuffer; - let framePixels = this.drawingContext.getImageData( + + let frameData = this.drawingContext.getImageData( 0, 0, this.width, this.height - ).data; - print(framePixels); - const imageData = new ImageData(framePixels, pImg.width, pImg.height); - pImg.drawingContext.putImageData(imageData, 0, 0); + ); + + pImg.drawingContext.putImageData(frameData, 0, 0); + + frames.push({ + image: pImg.drawingContext.getImageData(0, 0, pImg.width, pImg.height), + delay: 10 * 10 //GIF stores delay in one-hundredth of a second, shift to ms + }); count++; + print('Processing frame: ' + count); print('Frame count: ' + this.frameCount); } - print(pImg); - saveGif(pImg, fileName); - - print('No loop'); - noLoop(); - frameBuffer = []; + pImg.drawingContext.putImageData(frames[0].image, 0, 0); + pImg.gifProperties = { + displayIndex: 0, + loopLimit: 0, // let it loop indefinitely + loopCount: 0, + frames: frames, + numFrames: nFrames, + playing: true, + timeDisplayed: 0, + lastChangeTime: 0 + }; + + p5.prototype.encodeAndDownloadGif(pImg, fileName); + + frames = []; }; /** From f84cedc894629e9f87c9b36db18e5260bb9dcbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 3 Jul 2022 17:35:27 +0200 Subject: [PATCH 027/177] we are now able to dowload gifs! --- lib/empty-example/sketch.js | 3 +-- src/image/loading_displaying.js | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index dc942874b9..635854228c 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -3,7 +3,6 @@ function setup() { // put setup code here createCanvas(600, 600); - frameRate(3); } function draw() { @@ -19,5 +18,5 @@ function draw() { } function mousePressed() { - saveGif('mySketch', 2, 2); + saveGif('mySketch', 5); } diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 3ae982b390..b9e446b792 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -195,7 +195,7 @@ p5.prototype.saveGif = function(...args) { print(frameRate, nFrames, nFramesDelay); var count = nFramesDelay; - this.frameCount = count; + // this.frameCount = count; // width * height * (r,g,b,a) * frames // var frameBuffer = new Uint8ClampedArray( @@ -226,7 +226,7 @@ p5.prototype.saveGif = function(...args) { frames.push({ image: pImg.drawingContext.getImageData(0, 0, pImg.width, pImg.height), - delay: 10 * 10 //GIF stores delay in one-hundredth of a second, shift to ms + delay: 2 / 100 //GIF stores delay in one-hundredth of a second, shift to ms }); count++; From 99d1a0e1b0a31732fe8a228b9627b4e8871ac605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 4 Jul 2022 18:05:20 +0200 Subject: [PATCH 028/177] create documentation, example and fix bug --- lib/empty-example/sketch.js | 10 ++-- src/image/loading_displaying.js | 82 +++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 635854228c..42f8194355 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,13 +1,13 @@ /* eslint-disable no-unused-vars */ function setup() { - // put setup code here - createCanvas(600, 600); + createCanvas(100, 100); + colorMode(HSL); } function draw() { - // put drawing code here - background(20); + let hue = map(sin(frameCount / 100), -1, 1, 0, 100); + background(hue, 40, 60); // print(frameRate()); circle( @@ -18,5 +18,5 @@ function draw() { } function mousePressed() { - saveGif('mySketch', 5); + saveGif('mySketch', 2); } diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index b9e446b792..cc9ed18f06 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -159,6 +159,54 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { return pImg; }; +/** + * Generates a gif of your current animation and downloads it to your computer! + * + * The duration argument specifies how many seconds you want to record from your animation. + * This value is then converted to the necessary number of frames to generate it. + * + * With the delay argument, you can tell the function to skip the first `delay` seconds + * of the animation, and then download the `duration` next seconds. This means that regardless + * of the value of `delay`, your gif will always be `duration` seconds long. + * + * @method saveGif + * @param {String} filename File name of your gif + * @param {String} duration Duration in seconds that you wish to capture from your sketch + * @param {String} delay Duration in seconds that you wish wait before starting to capture + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * colorMode(HSL); + * } + + * function draw() { + * // create some cool dynamic background + * let hue = map(sin(frameCount / 100), -1, 1, 0, 100); + * background(hue, 40, 60); + + * // create a circle that moves diagonally + * circle( + * 100 * sin(frameCount / 10) + width / 2, + * 100 * sin(frameCount / 10) + height / 2, + * 10 + * ); + * } + + * // you can put it in the mousePressed function, + * // or keyPressed for example + * function mousePressed() { + * // this will download the first two seconds of my animation! + * saveGif('mySketch', 2); + * } + * + *
+ * + * @alt + * image of the underside of a white umbrella and grided ceililng above + */ p5.prototype.saveGif = function(...args) { // process args // @@ -166,7 +214,6 @@ p5.prototype.saveGif = function(...args) { let fileName; let seconds; let delay; - // let callback; switch (args.length) { case 2: @@ -178,34 +225,28 @@ p5.prototype.saveGif = function(...args) { seconds = args[1]; delay = args[2]; break; - default: - fileName = args[0]; - seconds = args[1]; - delay = args[2]; - callback = args[3]; } if (!delay) { delay = 0; } - let frameRate = this._frameRate || this._targetFrameRate || 60; - let nFrames = ceil(seconds * frameRate); - let nFramesDelay = ceil(delay * frameRate); - print(frameRate, nFrames, nFramesDelay); + let _frameRate = this._frameRate || this._targetFrameRate || 60; + let nFrames = Math.ceil(seconds * _frameRate); + let nFramesDelay = Math.ceil(delay * _frameRate); + print(_frameRate, nFrames, nFramesDelay); var count = nFramesDelay; - // this.frameCount = count; - // width * height * (r,g,b,a) * frames - // var frameBuffer = new Uint8ClampedArray( - // this.width * this.height * 4 * nFrames - // ); let frames = []; let pImg = new p5.Image(this.width, this.height, this); noLoop(); + console.log( + 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' + ); + while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -225,14 +266,11 @@ p5.prototype.saveGif = function(...args) { pImg.drawingContext.putImageData(frameData, 0, 0); frames.push({ - image: pImg.drawingContext.getImageData(0, 0, pImg.width, pImg.height), - delay: 2 / 100 //GIF stores delay in one-hundredth of a second, shift to ms + image: pImg.drawingContext.getImageData(0, 0, this.width, this.height), + delay: 100 //GIF stores delay in one-hundredth of a second, shift to ms }); count++; - - print('Processing frame: ' + count); - print('Frame count: ' + this.frameCount); } pImg.drawingContext.putImageData(frames[0].image, 0, 0); @@ -247,8 +285,12 @@ p5.prototype.saveGif = function(...args) { lastChangeTime: 0 }; + console.info('Frames processed, encoding gif. This may take a while...'); + p5.prototype.encodeAndDownloadGif(pImg, fileName); + loop(); + frames = []; }; From cce628bf417edfe6bc425fac34cef1a581bb13cb Mon Sep 17 00:00:00 2001 From: Austin Slominski Date: Tue, 5 Jul 2022 11:31:13 -0600 Subject: [PATCH 029/177] moved describe() into setup(), fixing failed tests --- src/webgl/3d_primitives.js | 91 ++++++++++++++++---------------------- src/webgl/interaction.js | 37 +++++++--------- src/webgl/light.js | 43 +++++++++--------- src/webgl/loading.js | 9 ++-- src/webgl/material.js | 59 ++++++++++-------------- src/webgl/p5.Camera.js | 72 +++++++++++++----------------- src/webgl/p5.Shader.js | 8 ++-- 7 files changed, 136 insertions(+), 183 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 779af9491c..ef6a973cb0 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -27,14 +27,13 @@ import * as constants from '../core/constants'; * // with width 50 and height 50 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white plane with black wireframe lines'); * } * * function draw() { * background(200); * plane(50, 50); * } - * - * describe('a white plane with black wireframe lines'); * * * @@ -114,6 +113,7 @@ p5.prototype.plane = function(width, height, detailX, detailY) { * // with width, height and depth of 50 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white box rotating in 3D space'); * } * * function draw() { @@ -122,8 +122,6 @@ p5.prototype.plane = function(width, height, detailX, detailY) { * rotateY(frameCount * 0.01); * box(50); * } - * - * describe('a white box rotating in 3D space'); * * */ @@ -235,14 +233,13 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * // draw a sphere with radius 40 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white sphere with black wireframe lines'); * } * * function draw() { * background(205, 102, 94); * sphere(40); * } - * - * describe('a white sphere with black wireframe lines'); * * * @@ -256,6 +253,9 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * detailX = createSlider(3, 24, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a white sphere with low detail on the x-axis, including a slider to adjust detailX' + * ); * } * * function draw() { @@ -263,10 +263,6 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * rotateY(millis() / 1000); * sphere(40, detailX.value(), 16); * } - * - * describe( - * 'a white sphere with low detail on the x-axis, including a slider to adjust detailX' - * ); * * * @@ -280,6 +276,9 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * detailY = createSlider(3, 16, 3); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a white sphere with low detail on the y-axis, including a slider to adjust detailY' + * ); * } * * function draw() { @@ -287,10 +286,6 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * rotateY(millis() / 1000); * sphere(40, 16, detailY.value()); * } - * - * describe( - * 'a white sphere with low detail on the y-axis, including a slider to adjust detailY' - * ); * * */ @@ -455,6 +450,7 @@ const _truncatedCone = function( * // with radius 20 and height 50 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a rotating white cylinder'); * } * * function draw() { @@ -463,8 +459,6 @@ const _truncatedCone = function( * rotateZ(frameCount * 0.01); * cylinder(20, 50); * } - * - * describe('a rotating white cylinder'); * * * @@ -478,6 +472,9 @@ const _truncatedCone = function( * detailX = createSlider(3, 24, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white cylinder with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -485,10 +482,6 @@ const _truncatedCone = function( * rotateY(millis() / 1000); * cylinder(20, 75, detailX.value(), 1); * } - * - * describe( - * 'a rotating white cylinder with limited X detail, with a slider that adjusts detailX' - * ); * * * @@ -502,6 +495,9 @@ const _truncatedCone = function( * detailY = createSlider(1, 16, 1); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white cylinder with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -509,10 +505,6 @@ const _truncatedCone = function( * rotateY(millis() / 1000); * cylinder(20, 75, 16, detailY.value()); * } - * - * describe( - * 'a rotating white cylinder with limited Y detail, with a slider that adjusts detailY' - * ); * * */ @@ -600,6 +592,7 @@ p5.prototype.cylinder = function( * // with radius 40 and height 70 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a rotating white cone'); * } * * function draw() { @@ -608,8 +601,6 @@ p5.prototype.cylinder = function( * rotateZ(frameCount * 0.01); * cone(40, 70); * } - * - * describe('a rotating white cone'); * * * @@ -623,6 +614,9 @@ p5.prototype.cylinder = function( * detailX = createSlider(3, 16, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white cone with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -630,10 +624,6 @@ p5.prototype.cylinder = function( * rotateY(millis() / 1000); * cone(30, 65, detailX.value(), 16); * } - * - * describe( - * 'a rotating white cone with limited X detail, with a slider that adjusts detailX' - * ); * * * @@ -647,6 +637,9 @@ p5.prototype.cylinder = function( * detailY = createSlider(3, 16, 3); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white cone with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -654,10 +647,6 @@ p5.prototype.cylinder = function( * rotateY(millis() / 1000); * cone(30, 65, 16, detailY.value()); * } - * - * describe( - * 'a rotating white cone with limited Y detail, with a slider that adjusts detailY' - * ); * * */ @@ -726,14 +715,13 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * // with radius 30, 40 and 40. * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white 3d ellipsoid'); * } * * function draw() { * background(205, 105, 94); * ellipsoid(30, 40, 40); * } - * - * describe('a white 3d ellipsoid'); * * * @@ -747,6 +735,9 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * detailX = createSlider(2, 24, 12); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white ellipsoid with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -754,10 +745,6 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * rotateY(millis() / 1000); * ellipsoid(30, 40, 40, detailX.value(), 8); * } - * - * describe( - * 'a rotating white ellipsoid with limited X detail, with a slider that adjusts detailX' - * ); * * * @@ -771,6 +758,9 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * detailY = createSlider(2, 24, 6); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white ellipsoid with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -778,10 +768,6 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * rotateY(millis() / 1000); * ellipsoid(30, 40, 40, 12, detailY.value()); * } - * - * describe( - * 'a rotating white ellipsoid with limited Y detail, with a slider that adjusts detailY' - * ); * * */ @@ -870,6 +856,7 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * // with ring radius 30 and tube radius 15 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a rotating white torus'); * } * * function draw() { @@ -878,8 +865,6 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * rotateY(frameCount * 0.01); * torus(30, 15); * } - * - * describe('a rotating white torus'); * * * @@ -893,6 +878,9 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * detailX = createSlider(3, 24, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white torus with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -900,10 +888,6 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * rotateY(millis() / 1000); * torus(30, 15, detailX.value(), 12); * } - * - * describe( - * 'a rotating white torus with limited X detail, with a slider that adjusts detailX' - * ); * * * @@ -917,6 +901,9 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * detailY = createSlider(3, 16, 3); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white torus with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -924,10 +911,6 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * rotateY(millis() / 1000); * torus(30, 15, 16, detailY.value()); * } - * - * describe( - * 'a rotating white torus with limited Y detail, with a slider that adjusts detailY' - * ); * * */ diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index 26d8a32493..d13a9acd3e 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -30,6 +30,9 @@ import * as constants from '../core/constants'; * function setup() { * createCanvas(100, 100, WEBGL); * normalMaterial(); + * describe( + * 'Camera orbits around a box when mouse is hold-clicked & then moved.' + * ); * } * function draw() { * background(200); @@ -37,7 +40,6 @@ import * as constants from '../core/constants'; * rotateY(0.5); * box(30, 50); * } - * describe('Camera orbits around a box when mouse is hold-clicked & then moved.'); * * * @@ -168,6 +170,9 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(); + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' + * ); * } * * function draw() { @@ -179,9 +184,6 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * noDebugMode(); * } * } - * describe( - * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' - * ); * * * @alt @@ -198,6 +200,7 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(GRID); + * describe('a 3D box is centered on a grid in a 3D sketch.'); * } * * function draw() { @@ -205,8 +208,6 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * orbitControl(); * box(15, 30); * } - * - * describe('a 3D box is centered on a grid in a 3D sketch.'); * * * @alt @@ -220,6 +221,9 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(AXES); + * describe( + * 'a 3D box is centered in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' + * ); * } * * function draw() { @@ -227,10 +231,6 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * orbitControl(); * box(15, 30); * } - * - * describe( - * 'a 3D box is centered in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' - * ); * * * @alt @@ -246,6 +246,7 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(GRID, 100, 10, 0, 0, 0); + * describe('a 3D box is centered on a grid in a 3D sketch'); * } * * function draw() { @@ -253,8 +254,6 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * orbitControl(); * box(15, 30); * } - * - * describe('a 3D box is centered on a grid in a 3D sketch'); * * * @alt @@ -268,6 +267,9 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(100, 10, 0, 0, 0, 20, 0, -40, 0); + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' + * ); * } * * function draw() { @@ -279,10 +281,6 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * stroke(255, 0, 150); * strokeWeight(0.8); * } - * - * describe( - * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' - * ); * * * @alt @@ -377,6 +375,9 @@ p5.prototype.debugMode = function(...args) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(); + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' + * ); * } * * function draw() { @@ -388,10 +389,6 @@ p5.prototype.debugMode = function(...args) { * noDebugMode(); * } * } - * - * describe( - * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' - * ); * * * @alt diff --git a/src/webgl/light.js b/src/webgl/light.js index 010df053e5..c5df0add0a 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -37,6 +37,7 @@ import * as constants from '../core/constants'; * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe('sphere with coral color under black light'); * } * function draw() { * background(100); @@ -44,7 +45,6 @@ import * as constants from '../core/constants'; * ambientMaterial(255, 127, 80); // coral material * sphere(40); * } - * describe('sphere with coral color under black light'); * * * @alt @@ -56,6 +56,7 @@ import * as constants from '../core/constants'; * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe('sphere with coral color under white light'); * } * function draw() { * background(100); @@ -63,7 +64,6 @@ import * as constants from '../core/constants'; * ambientMaterial(255, 127, 80); // coral material * sphere(40); * } - * describe('sphere with coral color under white light'); * * * @alt @@ -146,6 +146,9 @@ p5.prototype.ambientLight = function(v1, v2, v3, a) { * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe( + * 'Sphere with specular highlight. Clicking the mouse toggles the specular highlight color between red and the default white.' + * ); * } * * function draw() { @@ -174,10 +177,6 @@ p5.prototype.ambientLight = function(v1, v2, v3, a) { * function mouseClicked() { * setRedSpecularColor = !setRedSpecularColor; * } - * - * describe( - * 'Sphere with specular highlight. Clicking the mouse toggles the specular highlight color between red and the default white.' - * ); * * * @@ -262,6 +261,9 @@ p5.prototype.specularColor = function(v1, v2, v3) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'scene with sphere and directional light. The direction of the light is controlled with the mouse position.' + * ); * } * function draw() { * background(0); @@ -272,9 +274,6 @@ p5.prototype.specularColor = function(v1, v2, v3) { * noStroke(); * sphere(40); * } - * describe( - * 'scene with sphere and directional light. The direction of the light is controlled with the mouse position.' - * ); * * * @@ -383,6 +382,9 @@ p5.prototype.directionalLight = function(v1, v2, v3, x, y, z) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'scene with sphere and point light. The position of the light is controlled with the mouse position.' + * ); * } * function draw() { * background(0); @@ -400,9 +402,6 @@ p5.prototype.directionalLight = function(v1, v2, v3, x, y, z) { * noStroke(); * sphere(40); * } - * describe( - * 'scene with sphere and point light. The position of the light is controlled with the mouse position.' - * ); * * * @@ -493,6 +492,7 @@ p5.prototype.pointLight = function(v1, v2, v3, x, y, z) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('the light is partially ambient and partially directional'); * } * function draw() { * background(0); @@ -502,7 +502,6 @@ p5.prototype.pointLight = function(v1, v2, v3, x, y, z) { * rotateZ(millis() / 1000); * box(); * } - * describe('the light is partially ambient and partially directional'); * * * @@ -547,6 +546,9 @@ p5.prototype.lights = function() { * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe( + * 'Two spheres with different falloff values show different intensity of light' + * ); * } * function draw() { * ortho(); @@ -570,9 +572,6 @@ p5.prototype.lights = function() { * sphere(20); * pop(); * } - * describe( - * 'Two spheres with different falloff values show different intensity of light' - * ); * * * @@ -670,6 +669,9 @@ p5.prototype.lightFalloff = function( * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'scene with sphere and spot light. The position of the light is controlled with the mouse position.' + * ); * } * function draw() { * background(0); @@ -688,9 +690,6 @@ p5.prototype.lightFalloff = function( * noStroke(); * sphere(40); * } - * describe( - * 'scene with sphere and spot light. The position of the light is controlled with the mouse position.' - * ); * * * @@ -1004,6 +1003,9 @@ p5.prototype.spotLight = function( * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'Three white spheres. Each appears as a different color due to lighting.' + * ); * } * function draw() { * background(200); @@ -1024,9 +1026,6 @@ p5.prototype.spotLight = function( * ambientMaterial(255); * sphere(13); * } - * describe( - * 'Three white spheres. Each appears as a different color due to lighting.' - * ); * * * diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 39ed1d9b0e..039eba7fea 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -50,6 +50,7 @@ import './p5.Geometry'; * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Vertically rotating 3-d octahedron.'); * } * * function draw() { @@ -58,8 +59,6 @@ import './p5.Geometry'; * rotateY(frameCount * 0.01); * model(octahedron); * } - * - * describe('Vertically rotating 3-d octahedron.'); * * * @@ -79,6 +78,7 @@ import './p5.Geometry'; * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Vertically rotating 3-d teapot with red, green and blue gradient.'); * } * * function draw() { @@ -89,8 +89,6 @@ import './p5.Geometry'; * normalMaterial(); // For effect * model(teapot); * } - * - * describe('Vertically rotating 3-d teapot with red, green and blue gradient.'); * * * @@ -606,6 +604,7 @@ function parseASCIISTL(model, lines) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Vertically rotating 3-d octahedron.'); * } * * function draw() { @@ -614,8 +613,6 @@ function parseASCIISTL(model, lines) { * rotateY(frameCount * 0.01); * model(octahedron); * } - * - * describe('Vertically rotating 3-d octahedron.'); * * * diff --git a/src/webgl/material.js b/src/webgl/material.js index 89078ea821..6694427a3b 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -45,14 +45,13 @@ import './p5.Texture'; * shader(mandel); * noStroke(); * mandel.setUniform('p', [-0.74364388703, 0.13182590421]); + * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * } * * function draw() { * mandel.setUniform('r', 1.5 * exp(-6.5 * (1 + sin(millis() / 2000)))); * quad(-1, -1, 1, -1, 1, 1, -1, 1); * } - * - * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * * * @@ -164,6 +163,7 @@ p5.prototype.loadShader = function( * * // 'p' is the center point of the Mandelbrot image * mandel.setUniform('p', [-0.74364388703, 0.13182590421]); + * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * } * * function draw() { @@ -171,8 +171,6 @@ p5.prototype.loadShader = function( * mandel.setUniform('r', 1.5 * exp(-6.5 * (1 + sin(millis() / 2000)))); * quad(-1, -1, 1, -1, 1, 1, -1, 1); * } - * - * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * * * @@ -237,6 +235,10 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * orangeBlue.setUniform('colorBackground', [0.226, 0.0, 0.615]); * * noStroke(); + * + * describe( + * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' + * ); * } * * function draw() { @@ -257,10 +259,6 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * function mouseClicked() { * showRedGreen = !showRedGreen; * } - * - * describe( - * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' - * ); * * * @@ -334,6 +332,10 @@ p5.prototype.shader = function(s) { * * // Create our shader * shaderProgram = createShader(vertSrc, fragSrc); + * + * describe( + * 'Two rotating cubes. The left one is painted using a custom (user-defined) shader, while the right one is painted using the default fill shader.' + * ); * } * * // prettier-ignore @@ -362,10 +364,6 @@ p5.prototype.shader = function(s) { * box(width / 4); * pop(); * } - * - * describe( - * 'Two rotating cubes. The left one is painted using a custom (user-defined) shader, while the right one is painted using the default fill shader.' - * ); * * * @alt @@ -404,6 +402,7 @@ p5.prototype.resetShader = function() { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('spinning cube with a texture from an image'); * } * * function draw() { @@ -415,8 +414,6 @@ p5.prototype.resetShader = function() { * texture(img); * box(width / 2); * } - * - * describe('spinning cube with a texture from an image'); * * * @alt @@ -431,6 +428,7 @@ p5.prototype.resetShader = function() { * createCanvas(100, 100, WEBGL); * pg = createGraphics(200, 200); * pg.textSize(75); + * describe('plane with a texture from an image created by createGraphics()'); * } * * function draw() { @@ -443,8 +441,6 @@ p5.prototype.resetShader = function() { * noStroke(); * plane(50); * } - * - * describe('plane with a texture from an image created by createGraphics()'); * * * @alt @@ -460,6 +456,7 @@ p5.prototype.resetShader = function() { * } * function setup() { * createCanvas(100, 100, WEBGL); + * describe('rectangle with video as texture'); * } * * function draw() { @@ -472,8 +469,6 @@ p5.prototype.resetShader = function() { * function mousePressed() { * vid.loop(); * } - * - * describe('rectangle with video as texture'); * * * @@ -491,6 +486,7 @@ p5.prototype.resetShader = function() { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('quad with a texture, mapped using normalized coordinates'); * } * * function draw() { @@ -504,8 +500,6 @@ p5.prototype.resetShader = function() { * vertex(-40, 40, 0, 1); * endShape(); * } - * - * describe('quad with a texture, mapped using normalized coordinates'); * * * @alt @@ -549,6 +543,7 @@ p5.prototype.texture = function(tex) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('quad with a texture, mapped using normalized coordinates'); * } * * function draw() { @@ -561,8 +556,6 @@ p5.prototype.texture = function(tex) { * vertex(-50, 50, 0, 1); * endShape(); * } - * - * describe('quad with a texture, mapped using normalized coordinates'); * * * @alt @@ -579,6 +572,7 @@ p5.prototype.texture = function(tex) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('quad with a texture, mapped using image coordinates'); * } * * function draw() { @@ -591,8 +585,6 @@ p5.prototype.texture = function(tex) { * vertex(-50, 50, 0, img.height); * endShape(); * } - * - * describe('quad with a texture, mapped using image coordinates'); * * * @alt @@ -639,6 +631,7 @@ p5.prototype.textureMode = function(mode) { * function setup() { * createCanvas(100, 100, WEBGL); * textureWrap(MIRROR); + * describe('an image of the rocky mountains repeated in mirrored tiles'); * } * * function draw() { @@ -664,8 +657,6 @@ p5.prototype.textureMode = function(mode) { * vertex(-1, -1, 0, 0, 0); * endShape(); * } - * - * describe('an image of the rocky mountains repeated in mirrored tiles'); * * * @@ -701,6 +692,7 @@ p5.prototype.textureWrap = function(wrapX, wrapY = wrapX) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Sphere with normal material'); * } * * function draw() { @@ -708,8 +700,6 @@ p5.prototype.textureWrap = function(wrapX, wrapY = wrapX) { * normalMaterial(); * sphere(40); * } - * - * describe('Sphere with normal material'); * * * @alt @@ -759,6 +749,7 @@ p5.prototype.normalMaterial = function(...args) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('sphere reflecting red, blue, and green light'); * } * function draw() { * background(0); @@ -767,7 +758,6 @@ p5.prototype.normalMaterial = function(...args) { * ambientMaterial(70, 130, 230); * sphere(40); * } - * describe('sphere reflecting red, blue, and green light'); * * * @alt @@ -780,6 +770,7 @@ p5.prototype.normalMaterial = function(...args) { * // so object only reflects it's red and blue components * function setup() { * createCanvas(100, 100, WEBGL); + * describe('box reflecting only red and blue light'); * } * function draw() { * background(70); @@ -787,7 +778,6 @@ p5.prototype.normalMaterial = function(...args) { * ambientMaterial(255); // white material * box(30); * } - * describe('box reflecting only red and blue light'); * * * @alt @@ -800,6 +790,7 @@ p5.prototype.normalMaterial = function(...args) { * // green, it does not reflect any light * function setup() { * createCanvas(100, 100, WEBGL); + * describe('box reflecting no light'); * } * function draw() { * background(70); @@ -807,7 +798,6 @@ p5.prototype.normalMaterial = function(...args) { * ambientMaterial(255, 0, 255); // magenta material * box(30); * } - * describe('box reflecting no light'); * * * @alt @@ -872,6 +862,7 @@ p5.prototype.ambientMaterial = function(v1, v2, v3) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('sphere with green emissive material'); * } * function draw() { * background(0); @@ -880,7 +871,6 @@ p5.prototype.ambientMaterial = function(v1, v2, v3) { * emissiveMaterial(130, 230, 0); * sphere(40); * } - * describe('sphere with green emissive material'); * * * @@ -948,6 +938,7 @@ p5.prototype.emissiveMaterial = function(v1, v2, v3, a) { * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe('torus with specular material'); * } * * function draw() { @@ -964,8 +955,6 @@ p5.prototype.emissiveMaterial = function(v1, v2, v3, a) { * shininess(50); * torus(30, 10, 64, 64); * } - * - * describe('torus with specular material'); * * * @alt @@ -1020,6 +1009,7 @@ p5.prototype.specularMaterial = function(v1, v2, v3, alpha) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('two spheres, one more shiny than the other'); * } * function draw() { * background(0); @@ -1036,7 +1026,6 @@ p5.prototype.specularMaterial = function(v1, v2, v3, alpha) { * shininess(20); * sphere(20); * } - * describe('two spheres, one more shiny than the other'); * * * @alt diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 03c3b0e91b..637ada8e69 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -45,6 +45,7 @@ import p5 from '../core/main'; * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a square moving closer and then away from the camera.'); * } * function draw() { * background(204); @@ -52,7 +53,6 @@ import p5 from '../core/main'; * camera(0, 0, 20 + sin(frameCount * 0.01) * 10, 0, 0, 0, 0, 1, 0); * plane(10, 10); * } - * describe('a square moving closer and then away from the camera.'); * * * @@ -83,6 +83,9 @@ import p5 from '../core/main'; * sliderGroup[i].position(10, height + h); * sliderGroup[i].style('width', '80px'); * } + * describe( + * 'White square repeatedly grows to fill canvas and then shrinks. An interactive example of a red cube with 3 sliders for moving it across x, y, z axis and 3 sliders for shifting its center.' + * ); * } * * function draw() { @@ -99,10 +102,6 @@ import p5 from '../core/main'; * fill(255, 102, 94); * box(85); * } - * - * describe( - * 'White square repeatedly grows to fill canvas and then shrinks. An interactive example of a red cube with 3 sliders for moving it across x, y, z axis and 3 sliders for shifting its center.' - * ); * * * @alt @@ -146,6 +145,9 @@ p5.prototype.camera = function(...args) { * function setup() { * createCanvas(100, 100, WEBGL); * perspective(PI / 3.0, width / height, 0.1, 500); + * describe( + * 'two colored 3D boxes move back and forth, rotating as mouse is dragged.' + * ); * } * function draw() { * background(200); @@ -165,9 +167,6 @@ p5.prototype.camera = function(...args) { * box(30); * pop(); * } - * describe( - * 'two colored 3D boxes move back and forth, rotating as mouse is dragged.' - * ); * * * @@ -211,6 +210,9 @@ p5.prototype.perspective = function(...args) { * function setup() { * createCanvas(100, 100, WEBGL); * ortho(-width / 2, width / 2, height / 2, -height / 2, 0, 500); + * describe( + * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' + * ); * } * function draw() { * background(200); @@ -228,9 +230,6 @@ p5.prototype.perspective = function(...args) { * box(30); * pop(); * } - * describe( - * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' - * ); * * * @@ -277,6 +276,9 @@ p5.prototype.ortho = function(...args) { * createCanvas(100, 100, WEBGL); * setAttributes('antialias', true); * frustum(-0.1, 0.1, -0.1, 0.1, 0.1, 200); + * describe( + * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' + * ); * } * function draw() { * background(200); @@ -294,9 +296,6 @@ p5.prototype.ortho = function(...args) { * box(30); * pop(); * } - * describe( - * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' - * ); * * * @@ -342,6 +341,7 @@ p5.prototype.frustum = function(...args) { * createCanvas(100, 100, WEBGL); * background(0); * camera = createCamera(); + * describe('An example that creates a camera and moves it around the box.'); * } * * function draw() { @@ -349,8 +349,6 @@ p5.prototype.frustum = function(...args) { * camera.setPosition(sin(frameCount / 60) * 200, 0, 100); * box(20); * } - * - * describe('An example that creates a camera and moves it around the box.'); * * * @alt @@ -414,6 +412,9 @@ p5.prototype.createCamera = function() { * cam = createCamera(); * // set initial pan angle * cam.pan(-0.8); + * describe( + * 'camera view pans left and right across a series of rotating 3D boxes.' + * ); * } * * function draw() { @@ -443,10 +444,6 @@ p5.prototype.createCamera = function() { * translate(35, 0, 0); * box(20); * } - * - * describe( - * 'camera view pans left and right across a series of rotating 3D boxes.' - * ); * * * @@ -475,6 +472,7 @@ p5.Camera = function(renderer) { * cam = createCamera(); * div = createDiv(); * div.position(0, 0); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -482,8 +480,6 @@ p5.Camera = function(renderer) { * box(10); * div.html('eyeX = ' + cam.eyeX); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -504,6 +500,7 @@ p5.Camera = function(renderer) { * cam = createCamera(); * div = createDiv(); * div.position(0, 0); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -511,8 +508,6 @@ p5.Camera = function(renderer) { * box(10); * div.html('eyeY = ' + cam.eyeY); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -533,6 +528,7 @@ p5.Camera = function(renderer) { * cam = createCamera(); * div = createDiv(); * div.position(0, 0); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -540,8 +536,6 @@ p5.Camera = function(renderer) { * box(10); * div.html('eyeZ = ' + cam.eyeZ); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -564,14 +558,13 @@ p5.Camera = function(renderer) { * div = createDiv('centerX = ' + cam.centerX); * div.position(0, 0); * div.style('color', 'white'); + * describe('An example showing the use of camera object properties'); * } * * function draw() { * orbitControl(); * box(10); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -594,14 +587,13 @@ p5.Camera = function(renderer) { * div = createDiv('centerY = ' + cam.centerY); * div.position(0, 0); * div.style('color', 'white'); + * describe('An example showing the use of camera object properties'); * } * * function draw() { * orbitControl(); * box(10); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -624,14 +616,13 @@ p5.Camera = function(renderer) { * div = createDiv('centerZ = ' + cam.centerZ); * div.position(0, 0); * div.style('color', 'white'); + * describe('An example showing the use of camera object properties'); * } * * function draw() { * orbitControl(); * box(10); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -654,9 +645,8 @@ p5.Camera = function(renderer) { * div.position(0, 0); * div.style('color', 'blue'); * div.style('font-size', '18px'); + * describe('An example showing the use of camera object properties'); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -679,9 +669,8 @@ p5.Camera = function(renderer) { * div.position(0, 0); * div.style('color', 'blue'); * div.style('font-size', '18px'); + * describe('An example showing the use of camera object properties'); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -704,9 +693,8 @@ p5.Camera = function(renderer) { * div.position(0, 0); * div.style('color', 'blue'); * div.style('font-size', '18px'); + * describe('An example showing the use of camera object properties'); * } - * - * describe('An example showing the use of camera object properties'); * * * @alt @@ -1782,6 +1770,10 @@ p5.Camera.prototype._isActive = function() { * * // set variable for previously active camera: * currentCamera = 1; + * + * describe( + * 'Canvas switches between two camera views, each showing a series of spinning 3D boxes.' + * ); * } * * function draw() { @@ -1822,10 +1814,6 @@ p5.Camera.prototype._isActive = function() { * translate(35, 0, 0); * box(20); * } - * - * describe( - * 'Canvas switches between two camera views, each showing a series of spinning 3D boxes.' - * ); * * * diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index a4014b31af..5435814f2d 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -341,6 +341,10 @@ p5.Shader.prototype.useProgram = function() { * createCanvas(100, 100, WEBGL); * shader(grad); * noStroke(); + * + * describe( + * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' + * ); * } * * function draw() { @@ -363,10 +367,6 @@ p5.Shader.prototype.useProgram = function() { * function mouseClicked() { * showRedGreen = !showRedGreen; * } - * - * describe( - * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' - * ); * * * From 0ef2ecc229925667e84aea43f23566066f528b16 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Wed, 6 Jul 2022 21:36:27 -0700 Subject: [PATCH 030/177] Update Welcome bot first issue message --- .github/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/config.yml b/.github/config.yml index 1d0f4cae3d..c4629c14ed 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -4,8 +4,7 @@ # Comment to be posted to on first time issues newIssueWelcomeComment: > - Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to be sure to fill out the issue form template inputs accurately if you haven't already. - + Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, please make sure to fill out the inputs in the issue forms. Thank you! # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome # Comment to be posted to on PRs from first time contributors in your repository From 3646c531c6fa36ba45ae2c20615a522b897d0c43 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Jul 2022 17:50:01 +0000 Subject: [PATCH 031/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ae83abd20d..453180c560 100644 --- a/README.md +++ b/README.md @@ -564,6 +564,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Philip Bell

📖
tapioca24

🔌
Qianqian Ye

💻 🎨 📖 📋 👀 🌍 +
Adarsh

🌍 From 82bb3a8e644c334bcb0f917492e9d88dc00686b6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Jul 2022 17:50:02 +0000 Subject: [PATCH 032/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7efcd834f1..b5532c40f1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3082,6 +3082,15 @@ "review", "translation" ] + }, + { + "login": "adarrssh", + "name": "Adarsh", + "avatar_url": "https://avatars.githubusercontent.com/u/85433137?v=4", + "profile": "https://github.com/adarrssh", + "contributions": [ + "translation" + ] } ], "repoType": "github", From 6c5753e57c295a614ffb8ce2ef4f67c34124dd74 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Jul 2022 18:00:03 +0000 Subject: [PATCH 033/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 453180c560..275c21dd91 100644 --- a/README.md +++ b/README.md @@ -565,6 +565,7 @@ We recognize all types of contributions. This project follows the [all-contribut
tapioca24

🔌
Qianqian Ye

💻 🎨 📖 📋 👀 🌍
Adarsh

🌍 +
kaabe1

🎨 📋 From e650be4c90ce0fac7537be363d29cfb7ec979ed6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Jul 2022 18:00:04 +0000 Subject: [PATCH 034/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index b5532c40f1..8a10b86a7a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3091,6 +3091,16 @@ "contributions": [ "translation" ] + }, + { + "login": "kaabe1", + "name": "kaabe1", + "avatar_url": "https://avatars.githubusercontent.com/u/78185255?v=4", + "profile": "https://github.com/kaabe1", + "contributions": [ + "design", + "eventOrganizing" + ] } ], "repoType": "github", From 06856a2335df0797d8f42e5c78b0a34065d5ed3f Mon Sep 17 00:00:00 2001 From: evelyn masso Date: Thu, 7 Jul 2022 16:15:28 -0700 Subject: [PATCH 035/177] remove self from some steward areas --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 16f75510d5..4b74e10cab 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Stewards are contributors that are particularly involved, familiar, or responsiv Anyone interested can volunteer to be a steward! There are no specific requirements for expertise, just an interest in actively learning and participating. If you’re familiar with one or more parts of this project, open an issue to volunteer as a steward! * [@qianqianye](https://github.com/qianqianye) - project co-lead -* [@outofambit](https://github.com/outofambit) +* [@outofambit](https://github.com/outofambit) - project mentor * [@lmccart](https://github.com/lmccart) * [@limzykenneth](https://github.com/limzykenneth) * [@stalgiag](https://github.com/stalgiag) @@ -65,12 +65,12 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | Area | Steward(s) | | :-------------------------------- | :------------------------------------------- | -| Accessibility (Web Accessibility) | outofambit | -| Color | outofambit | -| Core/Environment/Rendering | outofambit
limzykenneth | +| Accessibility (Web Accessibility) | | +| Color | | +| Core/Environment/Rendering | limzykenneth | | Data | | | DOM | outofambit | -| Events | outofambit
limzykenneth | +| Events | limzykenneth | | Image | stalgiag | | IO | limzykenneth | | Math | limzykenneth | From a1aea892c2a91441a18025e7e3ef9eb3f409b8e6 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 7 Jul 2022 18:07:37 -0700 Subject: [PATCH 036/177] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b74e10cab..5583f83dc7 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Stewards are contributors that are particularly involved, familiar, or responsiv Anyone interested can volunteer to be a steward! There are no specific requirements for expertise, just an interest in actively learning and participating. If you’re familiar with one or more parts of this project, open an issue to volunteer as a steward! -* [@qianqianye](https://github.com/qianqianye) - project co-lead -* [@outofambit](https://github.com/outofambit) - project mentor +* [@qianqianye](https://github.com/qianqianye) - p5.js Project Lead +* [@outofambit](https://github.com/outofambit) - p5.js Mentor * [@lmccart](https://github.com/lmccart) * [@limzykenneth](https://github.com/limzykenneth) * [@stalgiag](https://github.com/stalgiag) From fdd288a0fa7bff323946d56f502310cd0cdf3511 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Jul 2022 03:01:03 +0000 Subject: [PATCH 037/177] docs: update README.md [skip ci] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b5490032bd..1f0dd5d7e3 100644 --- a/README.md +++ b/README.md @@ -567,6 +567,9 @@ We recognize all types of contributions. This project follows the [all-contribut
Adarsh

🌍
kaabe1

🎨 📋 + +
Seb Méndez

🌍 + From 6ca8d8761b5d107b9425d7f3ccf62e61c04d3683 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Jul 2022 03:01:03 +0000 Subject: [PATCH 038/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8a10b86a7a..fd4e9a7805 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3101,6 +3101,15 @@ "design", "eventOrganizing" ] + }, + { + "login": "Guirdo", + "name": "Seb Méndez", + "avatar_url": "https://avatars.githubusercontent.com/u/21044700?v=4", + "profile": "https://www.guirdo.xyz/", + "contributions": [ + "translation" + ] } ], "repoType": "github", From 4f0b3f65c663d5dca755526ce352a3f5b5012a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 8 Jul 2022 09:52:46 +0200 Subject: [PATCH 039/177] latest changes --- lib/empty-example/sketch.js | 224 +++++++++++++++++++++++++++++++- src/image/image.js | 1 - src/image/loading_displaying.js | 15 ++- 3 files changed, 227 insertions(+), 13 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 42f8194355..3ed12e63bc 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,22 +1,234 @@ /* eslint-disable no-unused-vars */ function setup() { - createCanvas(100, 100); - colorMode(HSL); + // put setup code here + createCanvas(300, 300); } function draw() { + // put drawing code here let hue = map(sin(frameCount / 100), -1, 1, 0, 100); - background(hue, 40, 60); + background(20); // print(frameRate()); + fill(250); circle( - 100 * sin(frameCount / 10) + width / 2, - 100 * sin(frameCount / 10) + height / 2, - 10 + 100 * sin(frameCount / 70) + width / 2, + 100 * sin(frameCount / 70) + height / 2, + 100 ); } function mousePressed() { saveGif('mySketch', 2); } + +/// COMPLEX SKETCH +// let offset; +// let spacing; + +// // let font; +// // function preload() { +// // font = loadFont("../SF-Mono-Regular.otf"); +// // } + +// function setup() { +// randomSeed(122); + +// w = min(windowHeight, windowWidth); +// createCanvas(w, w); + +// looping = false; +// saving = false; +// noLoop(); + +// divisor = random(1.2, 3).toFixed(2); + +// frameWidth = w / divisor; +// offset = (-frameWidth + w) / 2; + +// gen_num_total_squares = int(random(2, 20)); +// spacing = frameWidth / gen_num_total_squares; + +// initHue = random(0, 360); +// compColor = (initHue + 360 / random(1, 4)) % 360; + +// gen_stroke_weight = random(-100, 100); +// gen_stroke_fade_speed = random(30, 150); +// gen_shift_small_squares = random(0, 10); + +// gen_offset_small_sq_i = random(3, 10); +// gen_offset_small_sq_j = random(3, 10); + +// gen_rotation_speed = random(30, 250); + +// gen_depth = random(5, 20); +// gen_offset_i = random(1, 10); +// gen_offset_j = random(1, 10); + +// gen_transparency = random(20, 255); + +// background(24); +// } + +// function draw() { +// colorMode(HSB); +// background(initHue, 80, 20, gen_transparency); +// makeSquares(); +// // addHandle(); + +// if (saving) save('grid' + frameCount + '.png'); +// } + +// function makeSquares(depth = gen_depth) { +// colorMode(HSB); +// let count_i = 0; + +// for (let i = offset; i < w - offset; i += spacing) { +// let count_j = 0; +// count_i++; + +// if (count_i > gen_num_total_squares) break; + +// for (let j = offset; j < w - offset; j += spacing) { +// count_j++; + +// if (count_j > gen_num_total_squares) break; + +// for (let n = 0; n < depth; n++) { +// noFill(); + +// if (n === 0) { +// stroke(initHue, 100, 100); +// fill( +// initHue, +// 100, +// 100, +// map( +// sin( +// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed +// ), +// -1, +// 1, +// 0, +// 0.3 +// ) +// ); +// } else { +// stroke(compColor, map(n, 0, depth, 100, 0), 100); +// fill( +// compColor, +// 100, +// 100, +// map( +// cos( +// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed +// ), +// -1, +// 1, +// 0, +// 0.3 +// ) +// ); +// } + +// strokeWeight( +// map( +// sin( +// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed +// ), +// -1, +// 1, +// 0, +// 1.5 +// ) +// ); + +// push(); +// translate(i + spacing / 2, j + spacing / 2); + +// rotate( +// i * gen_offset_i + +// j * gen_offset_j + +// frameCount / (gen_rotation_speed / (n + 1)) +// ); + +// if (n % 2 !== 0) { +// translate( +// sin(frameCount / 50) * gen_shift_small_squares, +// cos(frameCount / 50) * gen_shift_small_squares +// ); +// rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); +// } + +// if (n > 0) +// rect( +// -spacing / (gen_offset_small_sq_i + n), +// -spacing / (gen_offset_small_sq_j + n), +// spacing / (n + 1), +// spacing / (n + 1) +// ); +// else rect(-spacing / 2, -spacing / 2, spacing, spacing); + +// pop(); +// } +// // strokeWeight(40); +// // point(i, j); +// } +// } +// } + +// function addHandle() { +// fill(40); +// noStroke(); +// textAlign(RIGHT, BOTTOM); +// textFont(font); +// textSize(20); +// text('@jesi_rgb', w - 30, w - 30); +// } + +// function mousePressed() { +// if (mouseButton === LEFT) { +// if (looping) { +// noLoop(); +// looping = false; +// } else { +// loop(); +// looping = true; +// } +// } +// } + +// function keyPressed() { +// console.log(key); +// switch (key) { +// // pressing the 's' key +// case 's': +// saveGif('mySketch', 2); +// break; + +// // pressing the '0' key +// case '0': +// frameCount = 0; +// loop(); +// noLoop(); +// break; + +// // pressing the ← key +// case 'ArrowLeft': +// frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); +// noLoop(); +// console.log(frameCount); +// break; + +// // pressing the → key +// case 'ArrowRights': +// frameCount += 1; +// noLoop(); +// console.log(frameCount); +// break; + +// default: +// break; +// } +// } diff --git a/src/image/image.js b/src/image/image.js index 6a3e9fa410..e2e6ef9954 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -302,7 +302,6 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { }; const gifWriter = new omggif.GifWriter(buffer, pImg.width, pImg.height, opts); let previousFrame = {}; - // Pass 2 // Determine if the frame needs a local palette // Also apply transparency optimization. This function will often blow up diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index cc9ed18f06..97ff0bed0f 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -231,7 +231,10 @@ p5.prototype.saveGif = function(...args) { delay = 0; } - let _frameRate = this._frameRate || this._targetFrameRate || 60; + let _frameRate = this._frameRate || this._targetFrameRate; + if (_frameRate === Infinity || _frameRate === undefined || _frameRate === 0) { + _frameRate = 60; + } let nFrames = Math.ceil(seconds * _frameRate); let nFramesDelay = Math.ceil(delay * _frameRate); print(_frameRate, nFrames, nFramesDelay); @@ -259,21 +262,21 @@ p5.prototype.saveGif = function(...args) { let frameData = this.drawingContext.getImageData( 0, 0, - this.width, - this.height + this.width * 2, + this.height * 2 ); - pImg.drawingContext.putImageData(frameData, 0, 0); + // pImg.drawingContext.putImageData(frameData, 0, 0); frames.push({ - image: pImg.drawingContext.getImageData(0, 0, this.width, this.height), + image: frameData, delay: 100 //GIF stores delay in one-hundredth of a second, shift to ms }); count++; } - pImg.drawingContext.putImageData(frames[0].image, 0, 0); + // pImg.drawingContext.putImageData(frames[0].image, 0, 0); pImg.gifProperties = { displayIndex: 0, loopLimit: 0, // let it loop indefinitely From 082a6de63e47c3fc5efc1e9cb96ada7306a364b7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 9 Jul 2022 08:50:48 +0000 Subject: [PATCH 040/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f0dd5d7e3..d8f106205a 100644 --- a/README.md +++ b/README.md @@ -569,6 +569,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Seb Méndez

🌍 +
Ryuya

🐛 👀 💻 From cebbc8fa1085cc349cabe7998ca5b663d196a089 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 9 Jul 2022 08:50:50 +0000 Subject: [PATCH 041/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index fd4e9a7805..504c12535d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3110,6 +3110,17 @@ "contributions": [ "translation" ] + }, + { + "login": "3ru", + "name": "Ryuya", + "avatar_url": "https://avatars.githubusercontent.com/u/69892552?v=4", + "profile": "https://github.com/3ru", + "contributions": [ + "bug", + "review", + "code" + ] } ], "repoType": "github", From 4a764bca7bcc01242471f4664c267c035a1f642b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 12 Jul 2022 05:08:49 +0000 Subject: [PATCH 042/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d8f106205a..5036994f65 100644 --- a/README.md +++ b/README.md @@ -570,6 +570,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Seb Méndez

🌍
Ryuya

🐛 👀 💻 +
LEMIBANDDEXARI

🌍 From 8a2f384a82e1e695a2febe9baadc3220d86bbc83 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 12 Jul 2022 05:08:50 +0000 Subject: [PATCH 043/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 504c12535d..09ef0e4b8a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3121,6 +3121,15 @@ "review", "code" ] + }, + { + "login": "LEMIBANDDEXARI", + "name": "LEMIBANDDEXARI", + "avatar_url": "https://avatars.githubusercontent.com/u/70129787?v=4", + "profile": "https://github.com/LEMIBANDDEXARI", + "contributions": [ + "translation" + ] } ], "repoType": "github", From cd5e83e3a568191fc5fd3d1be3006430046ba278 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Mon, 11 Jul 2022 22:38:57 -0700 Subject: [PATCH 044/177] Add stewards --- README.md | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5036994f65..576e896902 100644 --- a/README.md +++ b/README.md @@ -63,24 +63,25 @@ Anyone interested can volunteer to be a steward! There are no specific requireme * [@dhowe](https://github.com/dhowe) * [@rahulm2310](https://github.com/rahulm2310) -| Area | Steward(s) | -| :-------------------------------- | :------------------------------------------- | -| Accessibility (Web Accessibility) | | -| Color | | -| Core/Environment/Rendering | limzykenneth | -| Data | | -| DOM | outofambit | -| Events | limzykenneth | -| Image | stalgiag | -| IO | limzykenneth | -| Math | limzykenneth | -| Typography | dhowe | -| Utilities | | -| WebGL | stalgiag | -| Build Process/Unit Testing | outofambit | -| Localization Tools | outofambit | -| Friendly Errors | outofambit | -| [Website](https://github.com/processing/p5.js-website) | limzykenneth
rahulm2310 | +| Area | Steward(s) | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| Overall | [@qianqianye](https://github.com/qianqianye) | +| [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken) | +| [Color](https://github.com/processing/p5.js/tree/main/src/color) | | +| [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth) | +| [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken) | +| [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit) | +| [Events](https://github.com/processing/p5.js/tree/main/src/events) | [@limzykenneth](https://github.com/limzykenneth) | +| [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko) +| [IO](https://github.com/processing/p5.js/tree/main/src/io) | [@limzykenneth](https://github.com/limzykenneth) | +| [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth) | +| [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe) | +| [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken) | +| [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor) | +| Build Process/Unit Testing | [@outofambit](https://github.com/outofambit), [@kungfuchicken](https://github.com/kungfuchicken) | +| Localization Tools | [@outofambit](https://github.com/outofambit) | +| Friendly Errors | [@outofambit](https://github.com/outofambit) | +| [Contributor Docs](https://github.com/processing/p5.js/tree/main/contributor_docs) | [SoD 2022](https://github.com/processing/p5.js/wiki/Season-of-Docs-2022-Organization-Application---p5.js): [@limzykenneth](https://github.com/limzykenneth) | ## Contributors From 05c93858bcf2ed8f7b1c4362cd459c8312369112 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Tue, 12 Jul 2022 13:15:25 -0700 Subject: [PATCH 045/177] Add more stewards --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 576e896902..84a21d3b0c 100644 --- a/README.md +++ b/README.md @@ -67,17 +67,17 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Overall | [@qianqianye](https://github.com/qianqianye) | | [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken) | -| [Color](https://github.com/processing/p5.js/tree/main/src/color) | | -| [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth) | +| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP) | +| [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth), [@davepagurek](https://github.com/davepagurek), [@jeffawang](https://github.com/jeffawang) | | [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken) | -| [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit) | +| [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye) | | [Events](https://github.com/processing/p5.js/tree/main/src/events) | [@limzykenneth](https://github.com/limzykenneth) | -| [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko) +| [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko), [@KleoP](https://github.com/KleoP) | [IO](https://github.com/processing/p5.js/tree/main/src/io) | [@limzykenneth](https://github.com/limzykenneth) | -| [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth) | -| [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe) | +| [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth), [@jeffawang](https://github.com/jeffawang) | +| [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe), [@SarveshLimaye](https://github.com/SarveshLimaye) | | [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken) | -| [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor) | +| [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor); [@davepagurek](https://github.com/davepagurek); [@jeffawang](https://github.com/jeffawang) | | Build Process/Unit Testing | [@outofambit](https://github.com/outofambit), [@kungfuchicken](https://github.com/kungfuchicken) | | Localization Tools | [@outofambit](https://github.com/outofambit) | | Friendly Errors | [@outofambit](https://github.com/outofambit) | From d25214f091e9009440491cf8b0020d258c852a25 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Wed, 13 Jul 2022 14:06:55 -0700 Subject: [PATCH 046/177] Add more p5.js stewards --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 84a21d3b0c..c32035a5b4 100644 --- a/README.md +++ b/README.md @@ -67,20 +67,20 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Overall | [@qianqianye](https://github.com/qianqianye) | | [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken) | -| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP) | +| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese) | | [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth), [@davepagurek](https://github.com/davepagurek), [@jeffawang](https://github.com/jeffawang) | | [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken) | | [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye) | | [Events](https://github.com/processing/p5.js/tree/main/src/events) | [@limzykenneth](https://github.com/limzykenneth) | | [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko), [@KleoP](https://github.com/KleoP) | [IO](https://github.com/processing/p5.js/tree/main/src/io) | [@limzykenneth](https://github.com/limzykenneth) | -| [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth), [@jeffawang](https://github.com/jeffawang) | +| [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth), [@jeffawang](https://github.com/jeffawang), [@AdilRabbani](https://github.com/AdilRabbani) | | [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe), [@SarveshLimaye](https://github.com/SarveshLimaye) | | [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken) | -| [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor); [@davepagurek](https://github.com/davepagurek); [@jeffawang](https://github.com/jeffawang) | +| [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor); [@davepagurek](https://github.com/davepagurek); [@jeffawang](https://github.com/jeffawang); [@AdilRabbani](https://github.com/AdilRabbani) | | Build Process/Unit Testing | [@outofambit](https://github.com/outofambit), [@kungfuchicken](https://github.com/kungfuchicken) | -| Localization Tools | [@outofambit](https://github.com/outofambit) | -| Friendly Errors | [@outofambit](https://github.com/outofambit) | +| Localization Tools | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | +| Friendly Errors | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | | [Contributor Docs](https://github.com/processing/p5.js/tree/main/contributor_docs) | [SoD 2022](https://github.com/processing/p5.js/wiki/Season-of-Docs-2022-Organization-Application---p5.js): [@limzykenneth](https://github.com/limzykenneth) | ## Contributors From 9f066e08cc06730ec39ea420ecda720c32431419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 14 Jul 2022 09:30:06 +0200 Subject: [PATCH 047/177] solved problem with framerate --- src/image/loading_displaying.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 97ff0bed0f..c84b7ce8f1 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -262,15 +262,15 @@ p5.prototype.saveGif = function(...args) { let frameData = this.drawingContext.getImageData( 0, 0, - this.width * 2, - this.height * 2 + this.width, + this.height ); // pImg.drawingContext.putImageData(frameData, 0, 0); frames.push({ image: frameData, - delay: 100 //GIF stores delay in one-hundredth of a second, shift to ms + delay: 20 // 20 (which will then be converted to 2 inside the decoding function) is the minimum value that will work }); count++; From 43baa4838f6b961f370d1bd305f163610e60ce3a Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 14 Jul 2022 16:32:38 -0700 Subject: [PATCH 048/177] Add more stewards --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c32035a5b4..5ab476b575 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Overall | [@qianqianye](https://github.com/qianqianye) | | [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken) | -| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese) | +| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese), [@aahdee](https://github.com/aahdee) | | [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth), [@davepagurek](https://github.com/davepagurek), [@jeffawang](https://github.com/jeffawang) | | [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken) | | [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye) | From fdead358243be016b5b7940f9f1112cbf24ecda8 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 15 Jul 2022 05:50:31 +0000 Subject: [PATCH 049/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5ab476b575..75b176270c 100644 --- a/README.md +++ b/README.md @@ -572,6 +572,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Seb Méndez

🌍
Ryuya

🐛 👀 💻
LEMIBANDDEXARI

🌍 +
Vivek Tiwari

🌍 From 9e05f0894f839313f14745802ee76effd83f2f69 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 15 Jul 2022 05:50:32 +0000 Subject: [PATCH 050/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 09ef0e4b8a..f9314486de 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3130,6 +3130,15 @@ "contributions": [ "translation" ] + }, + { + "login": "probablyvivek", + "name": "Vivek Tiwari", + "avatar_url": "https://avatars.githubusercontent.com/u/25459353?v=4", + "profile": "https://linktr.ee/probablyvivek", + "contributions": [ + "translation" + ] } ], "repoType": "github", From fb8af2d818e9ba36cd74a94f67566eb23e6ae3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 15 Jul 2022 09:57:47 +0200 Subject: [PATCH 051/177] adding comments to saveGif function --- src/image/loading_displaying.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index c84b7ce8f1..9c74198db6 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -172,7 +172,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * @method saveGif * @param {String} filename File name of your gif * @param {String} duration Duration in seconds that you wish to capture from your sketch - * @param {String} delay Duration in seconds that you wish wait before starting to capture + * @param {String} delay Duration in seconds that you wish to wait before starting to capture * * @example *
@@ -209,12 +209,13 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { */ p5.prototype.saveGif = function(...args) { // process args - // let fileName; let seconds; let delay; + // this section takes care of parsing and processing + // the arguments in the correct format switch (args.length) { case 2: fileName = args[0]; @@ -231,20 +232,26 @@ p5.prototype.saveGif = function(...args) { delay = 0; } + // get the project's framerate + // if it is undefined or some non useful value, assume it's 60 let _frameRate = this._frameRate || this._targetFrameRate; if (_frameRate === Infinity || _frameRate === undefined || _frameRate === 0) { _frameRate = 60; } + + // because the input was in seconds, we now calculate + // how many frames those seconds translate to let nFrames = Math.ceil(seconds * _frameRate); let nFramesDelay = Math.ceil(delay * _frameRate); - print(_frameRate, nFrames, nFramesDelay); + // initialize variables for the frames processing var count = nFramesDelay; - let frames = []; let pImg = new p5.Image(this.width, this.height, this); noLoop(); + // we start on the frame set by the delay argument + frameCount = nFramesDelay; console.log( 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' @@ -254,7 +261,7 @@ p5.prototype.saveGif = function(...args) { /* we draw the next frame. this is important, since busy sketches or low end devices might take longer - to render the frame. So we just wait for the frame + to render some frames. So we just wait for the frame to be drawn and immediately save it to a buffer and continue */ redraw(); @@ -266,17 +273,17 @@ p5.prototype.saveGif = function(...args) { this.height ); - // pImg.drawingContext.putImageData(frameData, 0, 0); - frames.push({ image: frameData, - delay: 20 // 20 (which will then be converted to 2 inside the decoding function) is the minimum value that will work + delay: 20 + // 20 (which will then be converted to 2 inside the decoding function) + // is the minimum value that will work. Browsers will simply ignore + // values of 1. This is the smoothest GIF possible. }); count++; } - // pImg.drawingContext.putImageData(frames[0].image, 0, 0); pImg.gifProperties = { displayIndex: 0, loopLimit: 0, // let it loop indefinitely @@ -292,9 +299,9 @@ p5.prototype.saveGif = function(...args) { p5.prototype.encodeAndDownloadGif(pImg, fileName); - loop(); - frames = []; + + loop(); }; /** From 7f158a78b071c5bd39d742cf08aea92dbee6e375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 15 Jul 2022 09:58:18 +0200 Subject: [PATCH 052/177] adding constrain to powof2 to match the requirements of omggif --- src/image/image.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index e2e6ef9954..36f47ebacc 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -293,7 +293,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { while (powof2 < globalPalette.length) { powof2 <<= 1; } - globalPalette.length = powof2; + globalPalette.length = constrain(powof2, 2, 256); // global opts const opts = { @@ -370,7 +370,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { while (powof2 < palette.length) { powof2 <<= 1; } - palette.length = powof2; + palette.length = constrain(powof2, 2, 256); frameOpts.palette = new Uint32Array(palette); } if (i > 0) { From 2674e40248536efef623c177ab9000a4f6586990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 15 Jul 2022 09:58:40 +0200 Subject: [PATCH 053/177] some changes to sketch [not important] --- lib/empty-example/index.html | 1 + lib/empty-example/sketch.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/empty-example/index.html b/lib/empty-example/index.html index e8e44a5b0b..65f5f34363 100644 --- a/lib/empty-example/index.html +++ b/lib/empty-example/index.html @@ -9,6 +9,7 @@ body { padding: 0; margin: 0; + background-color: black; } diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 3ed12e63bc..3f9232f212 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -2,14 +2,16 @@ function setup() { // put setup code here - createCanvas(300, 300); + createCanvas(200, 200); } function draw() { // put drawing code here let hue = map(sin(frameCount / 100), -1, 1, 0, 100); - background(20); - // print(frameRate()); + background(hue); + + line(width / 2, 0, width / 2, height); + line(0, height / 2, width, height / 2); fill(250); circle( @@ -20,10 +22,12 @@ function draw() { } function mousePressed() { - saveGif('mySketch', 2); + if (mouseButton === RIGHT) { + saveGif('mySketch', 2, 3); + } } -/// COMPLEX SKETCH +// / COMPLEX SKETCH // let offset; // let spacing; @@ -33,7 +37,7 @@ function mousePressed() { // // } // function setup() { -// randomSeed(122); +// randomSeed(125); // w = min(windowHeight, windowWidth); // createCanvas(w, w); From 99def1eaf0d8f75c93f93cbd0b716411c8ff0993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 15 Jul 2022 20:39:05 +0200 Subject: [PATCH 054/177] fixed local palette being greater than 256 colors --- src/image/image.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/image/image.js b/src/image/image.js index 36f47ebacc..618aa58f15 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -310,6 +310,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { // transparent. We decide one particular color as transparent and make all // transparent pixels take this color. This helps in later in compression. for (let i = 0; i < props.numFrames; i++) { + print('FRAME:' + i.toString()); const localPaletteRequired = !framesUsingGlobalPalette.has(i); const palette = localPaletteRequired ? [] : globalPalette; const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height); @@ -323,7 +324,8 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { for (let k = 0; k < allFramesPixelColors[i].length; k++) { const color = allFramesPixelColors[i][k]; if (localPaletteRequired) { - if (colorIndicesLookup[color] === undefined) { + // local palette cannot be greater than 256 colors + if (colorIndicesLookup[color] === undefined && palette.length <= 256) { colorIndicesLookup[color] = palette.length; palette.push(color); } @@ -345,12 +347,15 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { // Transparency optimization const canBeTransparent = palette.filter(a => !cannotBeTransparent.has(a)); + print(canBeTransparent, canBeTransparent.length > 0); if (canBeTransparent.length > 0) { // Select a color to mark as transparent const transparent = canBeTransparent[0]; + print(localPaletteRequired ? 'local' : 'global'); const transparentIndex = localPaletteRequired ? colorIndicesLookup[transparent] : globalIndicesLookup[transparent]; + print(transparent, transparentIndex); if (i > 0) { for (let k = 0; k < allFramesPixelColors[i].length; k++) { // If this pixel in this frame has the same color in previous frame @@ -358,11 +363,13 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { pixelPaletteIndex[k] = transparentIndex; } } + frameOpts.transparent = transparentIndex; // If this frame has any transparency, do not dispose the previous frame previousFrame.frameOpts.disposal = 1; } } + frameOpts.delay = props.frames[i].delay / 10; // Move timing back into GIF formatting if (localPaletteRequired) { // force palette to be power of 2 @@ -375,6 +382,10 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { } if (i > 0) { // add the frame that came before the current one + // print('FRAME: ' + i.toString()); + print(previousFrame.frameOpts); + // print(''); + // print(''); gifWriter.addFrame( 0, 0, From f7c3a87b0d58fced2852d1a758a2226ac3159901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 15 Jul 2022 20:56:53 +0200 Subject: [PATCH 055/177] palette is now properly sized --- src/image/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image/image.js b/src/image/image.js index 618aa58f15..7273796bf0 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -325,7 +325,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const color = allFramesPixelColors[i][k]; if (localPaletteRequired) { // local palette cannot be greater than 256 colors - if (colorIndicesLookup[color] === undefined && palette.length <= 256) { + if (colorIndicesLookup[color] === undefined && palette.length <= 255) { colorIndicesLookup[color] = palette.length; palette.push(color); } From 0decfc05b011ebc2c08bd84acbe5c9efebc7dd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 15 Jul 2022 20:57:13 +0200 Subject: [PATCH 056/177] minor changes to follow the project's structure --- src/image/loading_displaying.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 9c74198db6..e0b79e2ee1 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -205,7 +205,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { *
* * @alt - * image of the underside of a white umbrella and grided ceililng above + * animation of a circle moving smoothly diagonally */ p5.prototype.saveGif = function(...args) { // process args @@ -234,7 +234,7 @@ p5.prototype.saveGif = function(...args) { // get the project's framerate // if it is undefined or some non useful value, assume it's 60 - let _frameRate = this._frameRate || this._targetFrameRate; + let _frameRate = this._targetFrameRate; if (_frameRate === Infinity || _frameRate === undefined || _frameRate === 0) { _frameRate = 60; } @@ -247,7 +247,7 @@ p5.prototype.saveGif = function(...args) { // initialize variables for the frames processing var count = nFramesDelay; let frames = []; - let pImg = new p5.Image(this.width, this.height, this); + let pImg = new p5.Image(this.width, this.height); noLoop(); // we start on the frame set by the delay argument @@ -257,6 +257,8 @@ p5.prototype.saveGif = function(...args) { 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' ); + let framePixels = new Uint8ClampedArray(this.width * this.height * 4); + while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -266,24 +268,35 @@ p5.prototype.saveGif = function(...args) { */ redraw(); - let frameData = this.drawingContext.getImageData( + const prevFrameData = this.drawingContext.getImageData( 0, 0, this.width, this.height ); + framePixels = prevFrameData.data; + + const imageData = new ImageData(framePixels, pImg.width, pImg.height); + pImg.drawingContext.putImageData(imageData, 0, 0); frames.push({ - image: frameData, + image: prevFrameData, delay: 20 - // 20 (which will then be converted to 2 inside the decoding function) - // is the minimum value that will work. Browsers will simply ignore - // values of 1. This is the smoothest GIF possible. }); + // frames.push({ + // image: framePixels, + // delay: 20 + // // 20 (which will then be converted to 2 inside the decoding function) + // // is the minimum value that will work. Browsers will simply ignore + // // values of 1. This is the smoothest GIF possible. + // }); + count++; } + print(frames[1]); + pImg.gifProperties = { displayIndex: 0, loopLimit: 0, // let it loop indefinitely @@ -297,11 +310,9 @@ p5.prototype.saveGif = function(...args) { console.info('Frames processed, encoding gif. This may take a while...'); - p5.prototype.encodeAndDownloadGif(pImg, fileName); - frames = []; - loop(); + p5.prototype.encodeAndDownloadGif(pImg, fileName); }; /** From ba661dce0465c2842d38752d097701f5762e88b5 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 16 Jul 2022 15:05:17 +0800 Subject: [PATCH 057/177] Fix noSmooth printing message when used with a non WebGL graphics --- src/core/shape/attributes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/shape/attributes.js b/src/core/shape/attributes.js index bf23b9c58e..84d6158bd3 100644 --- a/src/core/shape/attributes.js +++ b/src/core/shape/attributes.js @@ -103,11 +103,12 @@ p5.prototype.ellipseMode = function(m) { * 2 pixelated 36×36 white ellipses to left & right of center, black background */ p5.prototype.noSmooth = function() { - this.setAttributes('antialias', false); if (!this._renderer.isP3D) { if ('imageSmoothingEnabled' in this.drawingContext) { this.drawingContext.imageSmoothingEnabled = false; } + } else { + this.setAttributes('antialias', false); } return this; }; From 8d1d93490b67be9780eec9e3ece9e4a70f2146a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sat, 16 Jul 2022 12:27:36 +0200 Subject: [PATCH 058/177] mayor bugfix: we now save the whole canvas! --- src/image/loading_displaying.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index e0b79e2ee1..3a355439a6 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -247,7 +247,6 @@ p5.prototype.saveGif = function(...args) { // initialize variables for the frames processing var count = nFramesDelay; let frames = []; - let pImg = new p5.Image(this.width, this.height); noLoop(); // we start on the frame set by the delay argument @@ -257,7 +256,10 @@ p5.prototype.saveGif = function(...args) { 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' ); - let framePixels = new Uint8ClampedArray(this.width * this.height * 4); + const pd = this._pixelDensity; + const width_pd = this.width * pd; + const height_pd = this.height * pd; + let pImg = new p5.Image(width_pd, height_pd); while (count < nFrames + nFramesDelay) { /* @@ -271,27 +273,15 @@ p5.prototype.saveGif = function(...args) { const prevFrameData = this.drawingContext.getImageData( 0, 0, - this.width, - this.height + width_pd, + height_pd ); - framePixels = prevFrameData.data; - - const imageData = new ImageData(framePixels, pImg.width, pImg.height); - pImg.drawingContext.putImageData(imageData, 0, 0); frames.push({ image: prevFrameData, delay: 20 }); - // frames.push({ - // image: framePixels, - // delay: 20 - // // 20 (which will then be converted to 2 inside the decoding function) - // // is the minimum value that will work. Browsers will simply ignore - // // values of 1. This is the smoothest GIF possible. - // }); - count++; } From f1a15189cca64a395e6baf0c8849658b2cea9ceb Mon Sep 17 00:00:00 2001 From: KevinGrajeda Date: Sat, 16 Jul 2022 22:32:07 -0500 Subject: [PATCH 059/177] chainable removed in angleMode --- src/math/trigonometry.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index 979189d9f3..536d1d575d 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -284,7 +284,6 @@ p5.prototype.radians = angle => angle * constants.DEG_TO_RAD; * Calling angleMode() with no arguments returns current anglemode. * @method angleMode * @param {Constant} mode either RADIANS or DEGREES - * @chainable * @example *
* @@ -318,7 +317,6 @@ p5.prototype.angleMode = function(mode) { } else if (mode === constants.DEGREES || mode === constants.RADIANS) { this._angleMode = mode; } - return this; }; /** From 71a9165c7e0a3e30a0f9a0333b62e8bb855bcf32 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 18:31:17 +0000 Subject: [PATCH 060/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 75b176270c..5aa27edab9 100644 --- a/README.md +++ b/README.md @@ -573,6 +573,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Ryuya

🐛 👀 💻
LEMIBANDDEXARI

🌍
Vivek Tiwari

🌍 +
Kevin Grajeda

💻 From 67f371c27ea637317b2be8da6755963ad77e2c42 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 18:31:18 +0000 Subject: [PATCH 061/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index f9314486de..8513335ced 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3139,6 +3139,15 @@ "contributions": [ "translation" ] + }, + { + "login": "KevinGrajeda", + "name": "Kevin Grajeda", + "avatar_url": "https://avatars.githubusercontent.com/u/60023139?v=4", + "profile": "https://github.com/KevinGrajeda", + "contributions": [ + "code" + ] } ], "repoType": "github", From 6928988db407296a14856e98915a528b714fe2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Tue, 19 Jul 2022 14:04:55 +0200 Subject: [PATCH 062/177] sync commit --- src/image/image.js | 17 ++++++++--------- src/image/loading_displaying.js | 3 +-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index 7273796bf0..b0fa6074f5 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -265,10 +265,13 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const difference = palette.filter(x => !globalPaletteSet.has(x)); if (globalPalette.length + difference.length <= 256) { - for (let j = 0; j < difference.length; j++) { - globalPalette.push(difference[j]); - globalPaletteSet.add(difference[j]); - } + print(globalPalette.length); + globalPalette.concat(difference); + difference.forEach(v => globalPaletteSet.add(v)); + // for (let j = 0; j < difference.length; j++) { + // // globalPalette.push(difference[j]); + // globalPaletteSet.add(difference[j]); + // } // All frames using this palette now use the global palette framesUsingGlobalPalette = framesUsingGlobalPalette.concat( @@ -310,7 +313,6 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { // transparent. We decide one particular color as transparent and make all // transparent pixels take this color. This helps in later in compression. for (let i = 0; i < props.numFrames; i++) { - print('FRAME:' + i.toString()); const localPaletteRequired = !framesUsingGlobalPalette.has(i); const palette = localPaletteRequired ? [] : globalPalette; const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height); @@ -347,15 +349,12 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { // Transparency optimization const canBeTransparent = palette.filter(a => !cannotBeTransparent.has(a)); - print(canBeTransparent, canBeTransparent.length > 0); if (canBeTransparent.length > 0) { // Select a color to mark as transparent const transparent = canBeTransparent[0]; - print(localPaletteRequired ? 'local' : 'global'); const transparentIndex = localPaletteRequired ? colorIndicesLookup[transparent] : globalIndicesLookup[transparent]; - print(transparent, transparentIndex); if (i > 0) { for (let k = 0; k < allFramesPixelColors[i].length; k++) { // If this pixel in this frame has the same color in previous frame @@ -383,7 +382,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { if (i > 0) { // add the frame that came before the current one // print('FRAME: ' + i.toString()); - print(previousFrame.frameOpts); + // print(previousFrame.frameOpts); // print(''); // print(''); gifWriter.addFrame( diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 3a355439a6..8a062d7b59 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -256,6 +256,7 @@ p5.prototype.saveGif = function(...args) { 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' ); + pixelDensity(1); const pd = this._pixelDensity; const width_pd = this.width * pd; const height_pd = this.height * pd; @@ -285,8 +286,6 @@ p5.prototype.saveGif = function(...args) { count++; } - print(frames[1]); - pImg.gifProperties = { displayIndex: 0, loopLimit: 0, // let it loop indefinitely From 739385cb6b1cde0952513b0a57c7187766e15e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Tue, 19 Jul 2022 19:01:46 +0200 Subject: [PATCH 063/177] using gifenc to successfully encode gifs! --- package-lock.json | 6 + package.json | 3 +- src/image/image.js | 246 ++++---------------------------- src/image/loading_displaying.js | 56 ++++---- 4 files changed, 63 insertions(+), 248 deletions(-) diff --git a/package-lock.json b/package-lock.json index ffff484561..72d7ac4ea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6606,6 +6606,12 @@ } } }, + "gifenc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz", + "integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==", + "dev": true + }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", diff --git a/package.json b/package.json index 76acf7cb9b..f1d46de5ab 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "regenerator-runtime": "^0.13.3", "request": "^2.88.0", "simple-git": "^3.3.0", - "whatwg-fetch": "^2.0.4" + "whatwg-fetch": "^2.0.4", + "gifenc": "^1.0.3" }, "license": "LGPL-2.1", "main": "./lib/p5.min.js", diff --git a/src/image/image.js b/src/image/image.js index b0fa6074f5..a847231339 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -10,7 +10,8 @@ * for drawing images to the main display canvas. */ import p5 from '../core/main'; -import omggif from 'omggif'; +// import omggif from 'omggif'; +import { GIFEncoder, quantize, applyPalette } from 'gifenc'; /** * Creates a new p5.Image (the datatype for storing images). This provides a @@ -184,236 +185,41 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; -p5.prototype.encodeAndDownloadGif = function(pImg, filename) { - const props = pImg.gifProperties; +p5.prototype.encodeAndDownloadGif = async function(pImg, filename) { + const frames = pImg.gifProperties.frames; - //convert loopLimit back into Netscape Block formatting - let loopLimit = props.loopLimit; - if (loopLimit === 1) { - loopLimit = null; - } else if (loopLimit === null) { - loopLimit = 0; - } - const buffer = new Uint8Array(pImg.width * pImg.height * props.numFrames); - - const allFramesPixelColors = []; - - // Used to determine the occurrence of unique palettes and the frames - // which use them - const paletteFreqsAndFrames = {}; - - // Pass 1: - //loop over frames and get the frequency of each palette - for (let i = 0; i < props.numFrames; i++) { - const paletteSet = new Set(); - const data = props.frames[i].image.data; - const dataLength = data.length; - // The color for each pixel in this frame ( for easier lookup later ) - const pixelColors = new Uint32Array(pImg.width * pImg.height); - for (let j = 0, k = 0; j < dataLength; j += 4, k++) { - const r = data[j + 0]; - const g = data[j + 1]; - const b = data[j + 2]; - const color = (r << 16) | (g << 8) | (b << 0); - paletteSet.add(color); - - // What color does this pixel have in this frame ? - pixelColors[k] = color; - } - - // A way to put use the entire palette as an object key - const paletteStr = [...paletteSet].sort().toString(); - if (paletteFreqsAndFrames[paletteStr] === undefined) { - paletteFreqsAndFrames[paletteStr] = { freq: 1, frames: [i] }; - } else { - paletteFreqsAndFrames[paletteStr].freq += 1; - paletteFreqsAndFrames[paletteStr].frames.push(i); - } - - allFramesPixelColors.push(pixelColors); - } - - let framesUsingGlobalPalette = []; - - // Now to build the global palette - // Sort all the unique palettes in descending order of their occurrence - const palettesSortedByFreq = Object.keys(paletteFreqsAndFrames).sort(function( - a, - b - ) { - return paletteFreqsAndFrames[b].freq - paletteFreqsAndFrames[a].freq; - }); - - // The initial global palette is the one with the most occurrence - const globalPalette = palettesSortedByFreq[0] - .split(',') - .map(a => parseInt(a)); + // Setup an encoder that we will write frames into + const gif = GIFEncoder(); - framesUsingGlobalPalette = framesUsingGlobalPalette.concat( - paletteFreqsAndFrames[globalPalette].frames - ); + // We use for 'of' to loop with async await + for (let i = 0; i < frames.length; i++) { + console.info('Processing frame ' + i.toString()); + // Get RGBA data from canvas + const data = frames[i].image.data; - const globalPaletteSet = new Set(globalPalette); + // Choose a pixel format: rgba4444, rgb444, rgb565 + const format = 'rgba4444'; - // Build a more complete global palette - // Iterate over the remaining palettes in the order of - // their occurrence and see if the colors in this palette which are - // not in the global palette can be added there, while keeping the length - // of the global palette <= 256 - for (let i = 1; i < palettesSortedByFreq.length; i++) { - const palette = palettesSortedByFreq[i].split(',').map(a => parseInt(a)); + // If necessary, quantize your colors to a reduced palette + const palette = quantize(data, 256, { format, clearAlpha: false }); - const difference = palette.filter(x => !globalPaletteSet.has(x)); - if (globalPalette.length + difference.length <= 256) { - print(globalPalette.length); - globalPalette.concat(difference); - difference.forEach(v => globalPaletteSet.add(v)); - // for (let j = 0; j < difference.length; j++) { - // // globalPalette.push(difference[j]); - // globalPaletteSet.add(difference[j]); - // } + // Apply palette to RGBA data to get an indexed bitmap + const index = applyPalette(data, palette, format); - // All frames using this palette now use the global palette - framesUsingGlobalPalette = framesUsingGlobalPalette.concat( - paletteFreqsAndFrames[palettesSortedByFreq[i]].frames - ); - } - } - - framesUsingGlobalPalette = new Set(framesUsingGlobalPalette); + // Write frame into GIF + gif.writeFrame(index, width, height, { palette, delay: frames[i].delay }); - // Build a lookup table of the index of each color in the global palette - // Maps a color to its index - const globalIndicesLookup = {}; - for (let i = 0; i < globalPalette.length; i++) { - if (!globalIndicesLookup[globalPalette[i]]) { - globalIndicesLookup[globalPalette[i]] = i; - } - } - - // force palette to be power of 2 - let powof2 = 1; - while (powof2 < globalPalette.length) { - powof2 <<= 1; - } - globalPalette.length = constrain(powof2, 2, 256); - - // global opts - const opts = { - loop: loopLimit, - palette: new Uint32Array(globalPalette) - }; - const gifWriter = new omggif.GifWriter(buffer, pImg.width, pImg.height, opts); - let previousFrame = {}; - // Pass 2 - // Determine if the frame needs a local palette - // Also apply transparency optimization. This function will often blow up - // the size of a GIF if not for transparency. If a pixel in one frame has - // the same color in the previous frame, that pixel can be marked as - // transparent. We decide one particular color as transparent and make all - // transparent pixels take this color. This helps in later in compression. - for (let i = 0; i < props.numFrames; i++) { - const localPaletteRequired = !framesUsingGlobalPalette.has(i); - const palette = localPaletteRequired ? [] : globalPalette; - const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height); - - // Lookup table mapping color to its indices - const colorIndicesLookup = {}; - - // All the colors that cannot be marked transparent in this frame - const cannotBeTransparent = new Set(); - - for (let k = 0; k < allFramesPixelColors[i].length; k++) { - const color = allFramesPixelColors[i][k]; - if (localPaletteRequired) { - // local palette cannot be greater than 256 colors - if (colorIndicesLookup[color] === undefined && palette.length <= 255) { - colorIndicesLookup[color] = palette.length; - palette.push(color); - } - pixelPaletteIndex[k] = colorIndicesLookup[color]; - } else { - pixelPaletteIndex[k] = globalIndicesLookup[color]; - } - - if (i > 0) { - // If even one pixel of this color has changed in this frame - // from the previous frame, we cannot mark it as transparent - if (allFramesPixelColors[i - 1][k] !== color) { - cannotBeTransparent.add(color); - } - } - } - - const frameOpts = {}; - - // Transparency optimization - const canBeTransparent = palette.filter(a => !cannotBeTransparent.has(a)); - if (canBeTransparent.length > 0) { - // Select a color to mark as transparent - const transparent = canBeTransparent[0]; - const transparentIndex = localPaletteRequired - ? colorIndicesLookup[transparent] - : globalIndicesLookup[transparent]; - if (i > 0) { - for (let k = 0; k < allFramesPixelColors[i].length; k++) { - // If this pixel in this frame has the same color in previous frame - if (allFramesPixelColors[i - 1][k] === allFramesPixelColors[i][k]) { - pixelPaletteIndex[k] = transparentIndex; - } - } - - frameOpts.transparent = transparentIndex; - // If this frame has any transparency, do not dispose the previous frame - previousFrame.frameOpts.disposal = 1; - } - } - - frameOpts.delay = props.frames[i].delay / 10; // Move timing back into GIF formatting - if (localPaletteRequired) { - // force palette to be power of 2 - let powof2 = 1; - while (powof2 < palette.length) { - powof2 <<= 1; - } - palette.length = constrain(powof2, 2, 256); - frameOpts.palette = new Uint32Array(palette); - } - if (i > 0) { - // add the frame that came before the current one - // print('FRAME: ' + i.toString()); - // print(previousFrame.frameOpts); - // print(''); - // print(''); - gifWriter.addFrame( - 0, - 0, - pImg.width, - pImg.height, - previousFrame.pixelPaletteIndex, - previousFrame.frameOpts - ); - } - // previous frame object should now have details of this frame - previousFrame = { - pixelPaletteIndex, - frameOpts - }; + // Wait a tick so that we don't lock up browser + await new Promise(resolve => setTimeout(resolve, 0)); } - previousFrame.frameOpts.disposal = 1; - // add the last frame - gifWriter.addFrame( - 0, - 0, - pImg.width, - pImg.height, - previousFrame.pixelPaletteIndex, - previousFrame.frameOpts - ); + // Finalize stream + gif.finish(); + // Get a direct typed array view into the buffer to avoid copying it + const buffer = gif.bytesView(); const extension = 'gif'; - const blob = new Blob([buffer.slice(0, gifWriter.end())], { + const blob = new Blob([buffer], { type: 'image/gif' }); p5.prototype.downloadFile(blob, filename, extension); diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 8a062d7b59..15061176a2 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -10,6 +10,7 @@ import Filters from './filters'; import canvas from '../core/helpers'; import * as constants from '../core/constants'; import omggif from 'omggif'; +import { GIFEncoder, quantize, applyPalette } from 'gifenc'; import '../core/friendly_errors/validate_params'; import '../core/friendly_errors/file_errors'; @@ -207,7 +208,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * @alt * animation of a circle moving smoothly diagonally */ -p5.prototype.saveGif = function(...args) { +p5.prototype.saveGif = async function(...args) { // process args let fileName; @@ -246,13 +247,12 @@ p5.prototype.saveGif = function(...args) { // initialize variables for the frames processing var count = nFramesDelay; - let frames = []; noLoop(); // we start on the frame set by the delay argument frameCount = nFramesDelay; - console.log( + console.info( 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' ); @@ -260,8 +260,11 @@ p5.prototype.saveGif = function(...args) { const pd = this._pixelDensity; const width_pd = this.width * pd; const height_pd = this.height * pd; - let pImg = new p5.Image(width_pd, height_pd); + const gif = GIFEncoder(); + const format = 'rgba4444'; + + let p = createP('Frames processed: '); while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -271,37 +274,36 @@ p5.prototype.saveGif = function(...args) { */ redraw(); - const prevFrameData = this.drawingContext.getImageData( - 0, - 0, - width_pd, - height_pd - ); + const data = this.drawingContext.getImageData(0, 0, width_pd, height_pd) + .data; - frames.push({ - image: prevFrameData, - delay: 20 - }); + const palette = quantize(data, 256, { format }); + + // Apply palette to RGBA data to get an indexed bitmap + const index = applyPalette(data, palette, format); + // Write frame into GIF + gif.writeFrame(index, width, height, { palette, delay: 20 }); + + await new Promise(resolve => setTimeout(resolve, 0)); + p.text = 'Frames processed: ' + (count - nFramesDelay).toString(); count++; } - pImg.gifProperties = { - displayIndex: 0, - loopLimit: 0, // let it loop indefinitely - loopCount: 0, - frames: frames, - numFrames: nFrames, - playing: true, - timeDisplayed: 0, - lastChangeTime: 0 - }; - console.info('Frames processed, encoding gif. This may take a while...'); - frames = []; + gif.finish(); + loop(); - p5.prototype.encodeAndDownloadGif(pImg, fileName); + + // Get a direct typed array view into the buffer to avoid copying it + const buffer = gif.bytesView(); + const extension = 'gif'; + const blob = new Blob([buffer], { + type: 'image/gif' + }); + + p5.prototype.downloadFile(blob, fileName, extension); }; /** From 07ba8a444f2b6223620b61ec8fcb1e9f76c61a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Tue, 19 Jul 2022 23:21:55 +0200 Subject: [PATCH 064/177] remove async --- lib/empty-example/index.html | 2 +- lib/empty-example/sketch.js | 440 ++++++++++++++++---------------- src/image/image.js | 4 +- src/image/loading_displaying.js | 9 +- 4 files changed, 234 insertions(+), 221 deletions(-) diff --git a/lib/empty-example/index.html b/lib/empty-example/index.html index 65f5f34363..788b9f3ee3 100644 --- a/lib/empty-example/index.html +++ b/lib/empty-example/index.html @@ -9,7 +9,7 @@ body { padding: 0; margin: 0; - background-color: black; + background-color: #1b1b1b; } diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 3f9232f212..fa859fe8e7 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,238 +1,250 @@ /* eslint-disable no-unused-vars */ -function setup() { - // put setup code here - createCanvas(200, 200); -} +// function setup() { +// // put setup code here +// createCanvas(500, 500); +// } -function draw() { - // put drawing code here - let hue = map(sin(frameCount / 100), -1, 1, 0, 100); - background(hue); - - line(width / 2, 0, width / 2, height); - line(0, height / 2, width, height / 2); - - fill(250); - circle( - 100 * sin(frameCount / 70) + width / 2, - 100 * sin(frameCount / 70) + height / 2, - 100 - ); -} +// function draw() { +// // put drawing code here +// let hue = map(sin(frameCount / 10), -1, 1, 127, 255); +// let hue_2 = map(sin(frameCount / 100) + 0.791, -1, 1, 127, 255); + +// strokeWeight(0); +// line(width / 2, 0, width / 2, height); +// line(0, height / 2, width, height / 2); + +// fill(40, 40, hue); +// rect(0, 0, width / 2, height / 2); + +// // fill(80, 80, hue); +// rect(width / 2, 0, width / 2, height / 2); + +// fill(0, 180, hue); +// rect(0, height / 2, width / 2, height / 2); + +// // fill(240, 240, 0); +// rect(width / 2, height / 2, width / 2, height / 2); + +// fill(250); +// stroke(250, 250, 20); +// strokeWeight(4); +// circle( +// 100 * sin(frameCount / 20) + width / 2, +// // 100 * sin(frameCount / 20) + height / 2, +// // width / 2, +// height / 2, +// 100 +// ); +// } -function mousePressed() { - if (mouseButton === RIGHT) { - saveGif('mySketch', 2, 3); - } -} +// function mousePressed() { +// if (mouseButton === RIGHT) { +// saveGif('mySketch', 10, 3); +// } +// } // / COMPLEX SKETCH -// let offset; -// let spacing; - -// // let font; -// // function preload() { -// // font = loadFont("../SF-Mono-Regular.otf"); -// // } +let offset; +let spacing; -// function setup() { -// randomSeed(125); - -// w = min(windowHeight, windowWidth); -// createCanvas(w, w); +function setup() { + // randomSeed(125); -// looping = false; -// saving = false; -// noLoop(); + w = min(windowHeight, windowWidth); + createCanvas(w, w); + print(w); + looping = false; + saving = false; + noLoop(); -// divisor = random(1.2, 3).toFixed(2); + divisor = random(1.2, 3).toFixed(2); -// frameWidth = w / divisor; -// offset = (-frameWidth + w) / 2; + frameWidth = w / divisor; + offset = (-frameWidth + w) / 2; -// gen_num_total_squares = int(random(2, 20)); -// spacing = frameWidth / gen_num_total_squares; + gen_num_total_squares = int(random(2, 20)); + spacing = frameWidth / gen_num_total_squares; -// initHue = random(0, 360); -// compColor = (initHue + 360 / random(1, 4)) % 360; + initHue = random(0, 360); + compColor = (initHue + 360 / random(1, 4)) % 360; -// gen_stroke_weight = random(-100, 100); -// gen_stroke_fade_speed = random(30, 150); -// gen_shift_small_squares = random(0, 10); + gen_stroke_weight = random(-100, 100); + gen_stroke_fade_speed = random(30, 150); + gen_shift_small_squares = random(0, 10); -// gen_offset_small_sq_i = random(3, 10); -// gen_offset_small_sq_j = random(3, 10); + gen_offset_small_sq_i = random(3, 10); + gen_offset_small_sq_j = random(3, 10); -// gen_rotation_speed = random(30, 250); + gen_rotation_speed = random(30, 250); -// gen_depth = random(5, 20); -// gen_offset_i = random(1, 10); -// gen_offset_j = random(1, 10); + gen_depth = random(5, 20); + gen_offset_i = random(1, 10); + gen_offset_j = random(1, 10); -// gen_transparency = random(20, 255); + gen_transparency = random(20, 255); -// background(24); -// } + background(24); +} -// function draw() { -// colorMode(HSB); -// background(initHue, 80, 20, gen_transparency); -// makeSquares(); -// // addHandle(); +function draw() { + colorMode(HSB); + background(initHue, 80, 20, gen_transparency); + makeSquares(); + // addHandle(); -// if (saving) save('grid' + frameCount + '.png'); -// } + if (saving) save('grid' + frameCount + '.png'); +} -// function makeSquares(depth = gen_depth) { -// colorMode(HSB); -// let count_i = 0; - -// for (let i = offset; i < w - offset; i += spacing) { -// let count_j = 0; -// count_i++; - -// if (count_i > gen_num_total_squares) break; - -// for (let j = offset; j < w - offset; j += spacing) { -// count_j++; - -// if (count_j > gen_num_total_squares) break; - -// for (let n = 0; n < depth; n++) { -// noFill(); - -// if (n === 0) { -// stroke(initHue, 100, 100); -// fill( -// initHue, -// 100, -// 100, -// map( -// sin( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 0.3 -// ) -// ); -// } else { -// stroke(compColor, map(n, 0, depth, 100, 0), 100); -// fill( -// compColor, -// 100, -// 100, -// map( -// cos( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 0.3 -// ) -// ); -// } - -// strokeWeight( -// map( -// sin( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 1.5 -// ) -// ); - -// push(); -// translate(i + spacing / 2, j + spacing / 2); - -// rotate( -// i * gen_offset_i + -// j * gen_offset_j + -// frameCount / (gen_rotation_speed / (n + 1)) -// ); - -// if (n % 2 !== 0) { -// translate( -// sin(frameCount / 50) * gen_shift_small_squares, -// cos(frameCount / 50) * gen_shift_small_squares -// ); -// rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); -// } - -// if (n > 0) -// rect( -// -spacing / (gen_offset_small_sq_i + n), -// -spacing / (gen_offset_small_sq_j + n), -// spacing / (n + 1), -// spacing / (n + 1) -// ); -// else rect(-spacing / 2, -spacing / 2, spacing, spacing); - -// pop(); -// } -// // strokeWeight(40); -// // point(i, j); -// } -// } -// } +function makeSquares(depth = gen_depth) { + colorMode(HSB); + let count_i = 0; + + for (let i = offset; i < w - offset; i += spacing) { + let count_j = 0; + count_i++; + + if (count_i > gen_num_total_squares) break; + + for (let j = offset; j < w - offset; j += spacing) { + count_j++; + + if (count_j > gen_num_total_squares) break; + + for (let n = 0; n < depth; n++) { + noFill(); + + if (n === 0) { + stroke(initHue, 100, 100); + fill( + initHue, + 100, + 100, + map( + sin( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 0.3 + ) + ); + } else { + stroke(compColor, map(n, 0, depth, 100, 0), 100); + fill( + compColor, + 100, + 100, + map( + cos( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 0.3 + ) + ); + } + + strokeWeight( + map( + sin( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 1.5 + ) + ); + + push(); + translate(i + spacing / 2, j + spacing / 2); + + rotate( + i * gen_offset_i + + j * gen_offset_j + + frameCount / (gen_rotation_speed / (n + 1)) + ); + + if (n % 2 !== 0) { + translate( + sin(frameCount / 50) * gen_shift_small_squares, + cos(frameCount / 50) * gen_shift_small_squares + ); + rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); + } + + if (n > 0) + rect( + -spacing / (gen_offset_small_sq_i + n), + -spacing / (gen_offset_small_sq_j + n), + spacing / (n + 1), + spacing / (n + 1) + ); + else rect(-spacing / 2, -spacing / 2, spacing, spacing); + + pop(); + } + // strokeWeight(40); + // point(i, j); + } + } +} -// function addHandle() { -// fill(40); -// noStroke(); -// textAlign(RIGHT, BOTTOM); -// textFont(font); -// textSize(20); -// text('@jesi_rgb', w - 30, w - 30); -// } +function addHandle() { + fill(40); + noStroke(); + textAlign(RIGHT, BOTTOM); + textFont(font); + textSize(20); + text('@jesi_rgb', w - 30, w - 30); +} -// function mousePressed() { -// if (mouseButton === LEFT) { -// if (looping) { -// noLoop(); -// looping = false; -// } else { -// loop(); -// looping = true; -// } -// } -// } +function mousePressed() { + if (mouseButton === LEFT) { + if (looping) { + noLoop(); + looping = false; + } else { + loop(); + looping = true; + } + } +} -// function keyPressed() { -// console.log(key); -// switch (key) { -// // pressing the 's' key -// case 's': -// saveGif('mySketch', 2); -// break; - -// // pressing the '0' key -// case '0': -// frameCount = 0; -// loop(); -// noLoop(); -// break; - -// // pressing the ← key -// case 'ArrowLeft': -// frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); -// noLoop(); -// console.log(frameCount); -// break; - -// // pressing the → key -// case 'ArrowRights': -// frameCount += 1; -// noLoop(); -// console.log(frameCount); -// break; - -// default: -// break; -// } -// } +function keyPressed() { + console.log(key); + switch (key) { + // pressing the 's' key + case 's': + saveGif('mySketch', 3); + break; + + // pressing the '0' key + case '0': + frameCount = 0; + loop(); + noLoop(); + break; + + // pressing the ← key + case 'ArrowLeft': + frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); + noLoop(); + console.log(frameCount); + break; + + // pressing the → key + case 'ArrowRights': + frameCount += 1; + noLoop(); + console.log(frameCount); + break; + + default: + break; + } +} diff --git a/src/image/image.js b/src/image/image.js index a847231339..878f377119 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -185,7 +185,7 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; -p5.prototype.encodeAndDownloadGif = async function(pImg, filename) { +p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const frames = pImg.gifProperties.frames; // Setup an encoder that we will write frames into @@ -210,7 +210,7 @@ p5.prototype.encodeAndDownloadGif = async function(pImg, filename) { gif.writeFrame(index, width, height, { palette, delay: frames[i].delay }); // Wait a tick so that we don't lock up browser - await new Promise(resolve => setTimeout(resolve, 0)); + // await new Promise(resolve => setTimeout(resolve, 0)); } // Finalize stream diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 15061176a2..101a1b2d2e 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -208,7 +208,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * @alt * animation of a circle moving smoothly diagonally */ -p5.prototype.saveGif = async function(...args) { +p5.prototype.saveGif = function(...args) { // process args let fileName; @@ -264,7 +264,8 @@ p5.prototype.saveGif = async function(...args) { const gif = GIFEncoder(); const format = 'rgba4444'; - let p = createP('Frames processed: '); + // let p = createP('Frames processed: '); + while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -285,8 +286,8 @@ p5.prototype.saveGif = async function(...args) { // Write frame into GIF gif.writeFrame(index, width, height, { palette, delay: 20 }); - await new Promise(resolve => setTimeout(resolve, 0)); - p.text = 'Frames processed: ' + (count - nFramesDelay).toString(); + // await new Promise(resolve => setTimeout(resolve, 0)); + // p.text = 'Frames processed: ' + (count - nFramesDelay).toString(); count++; } From e13735566be0b1c48456110c6406ca109a3a92db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 20 Jul 2022 13:02:10 +0200 Subject: [PATCH 065/177] removing unnecesary code --- src/image/image.js | 41 --------------------------------- src/image/loading_displaying.js | 10 +++----- 2 files changed, 3 insertions(+), 48 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index 878f377119..50cc364da5 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -11,7 +11,6 @@ */ import p5 from '../core/main'; // import omggif from 'omggif'; -import { GIFEncoder, quantize, applyPalette } from 'gifenc'; /** * Creates a new p5.Image (the datatype for storing images). This provides a @@ -185,46 +184,6 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; -p5.prototype.encodeAndDownloadGif = function(pImg, filename) { - const frames = pImg.gifProperties.frames; - - // Setup an encoder that we will write frames into - const gif = GIFEncoder(); - - // We use for 'of' to loop with async await - for (let i = 0; i < frames.length; i++) { - console.info('Processing frame ' + i.toString()); - // Get RGBA data from canvas - const data = frames[i].image.data; - - // Choose a pixel format: rgba4444, rgb444, rgb565 - const format = 'rgba4444'; - - // If necessary, quantize your colors to a reduced palette - const palette = quantize(data, 256, { format, clearAlpha: false }); - - // Apply palette to RGBA data to get an indexed bitmap - const index = applyPalette(data, palette, format); - - // Write frame into GIF - gif.writeFrame(index, width, height, { palette, delay: frames[i].delay }); - - // Wait a tick so that we don't lock up browser - // await new Promise(resolve => setTimeout(resolve, 0)); - } - - // Finalize stream - gif.finish(); - - // Get a direct typed array view into the buffer to avoid copying it - const buffer = gif.bytesView(); - const extension = 'gif'; - const blob = new Blob([buffer], { - type: 'image/gif' - }); - p5.prototype.downloadFile(blob, filename, extension); -}; - /** * Capture a sequence of frames that can be used to create a movie. * Accepts a callback. For example, you may wish to send the frames diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 101a1b2d2e..433e2a5769 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -264,11 +264,9 @@ p5.prototype.saveGif = function(...args) { const gif = GIFEncoder(); const format = 'rgba4444'; - // let p = createP('Frames processed: '); - while (count < nFrames + nFramesDelay) { - /* - we draw the next frame. this is important, since + /* + we draw the next frame. this is important, since busy sketches or low end devices might take longer to render some frames. So we just wait for the frame to be drawn and immediately save it to a buffer and continue @@ -286,14 +284,12 @@ p5.prototype.saveGif = function(...args) { // Write frame into GIF gif.writeFrame(index, width, height, { palette, delay: 20 }); - // await new Promise(resolve => setTimeout(resolve, 0)); - // p.text = 'Frames processed: ' + (count - nFramesDelay).toString(); count++; } console.info('Frames processed, encoding gif. This may take a while...'); - gif.finish(); + // gif.finish(); loop(); From 6632a24e688911bd5c15fbb8cf97074b723d683a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 20 Jul 2022 13:10:09 +0200 Subject: [PATCH 066/177] undo weird change --- src/image/image.js | 10 ++++------ src/image/loading_displaying.js | 11 +++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index b0fa6074f5..6c08eb0997 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -266,12 +266,10 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const difference = palette.filter(x => !globalPaletteSet.has(x)); if (globalPalette.length + difference.length <= 256) { print(globalPalette.length); - globalPalette.concat(difference); - difference.forEach(v => globalPaletteSet.add(v)); - // for (let j = 0; j < difference.length; j++) { - // // globalPalette.push(difference[j]); - // globalPaletteSet.add(difference[j]); - // } + for (let j = 0; j < difference.length; j++) { + globalPalette.push(difference[j]); + globalPaletteSet.add(difference[j]); + } // All frames using this palette now use the global palette framesUsingGlobalPalette = framesUsingGlobalPalette.concat( diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 8a062d7b59..577740cdb2 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -248,15 +248,14 @@ p5.prototype.saveGif = function(...args) { var count = nFramesDelay; let frames = []; - noLoop(); + this.noLoop(); // we start on the frame set by the delay argument - frameCount = nFramesDelay; - + this.frameCount = nFramesDelay; console.log( 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' ); - pixelDensity(1); + this.pixelDensity(1); const pd = this._pixelDensity; const width_pd = this.width * pd; const height_pd = this.height * pd; @@ -269,7 +268,7 @@ p5.prototype.saveGif = function(...args) { to render some frames. So we just wait for the frame to be drawn and immediately save it to a buffer and continue */ - redraw(); + this.redraw(); const prevFrameData = this.drawingContext.getImageData( 0, @@ -300,7 +299,7 @@ p5.prototype.saveGif = function(...args) { console.info('Frames processed, encoding gif. This may take a while...'); frames = []; - loop(); + this.loop(); p5.prototype.encodeAndDownloadGif(pImg, fileName); }; From 10dc92627d614285716ddf2f24f12c1f972b0e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 20 Jul 2022 13:10:42 +0200 Subject: [PATCH 067/177] removing and cleaning --- src/image/image.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/image/image.js b/src/image/image.js index 6c08eb0997..270b80a1f5 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -265,7 +265,6 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const difference = palette.filter(x => !globalPaletteSet.has(x)); if (globalPalette.length + difference.length <= 256) { - print(globalPalette.length); for (let j = 0; j < difference.length; j++) { globalPalette.push(difference[j]); globalPaletteSet.add(difference[j]); @@ -378,11 +377,6 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { frameOpts.palette = new Uint32Array(palette); } if (i > 0) { - // add the frame that came before the current one - // print('FRAME: ' + i.toString()); - // print(previousFrame.frameOpts); - // print(''); - // print(''); gifWriter.addFrame( 0, 0, From b495c6798b629675535a7772d10561a6712f7a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 20 Jul 2022 13:55:57 +0200 Subject: [PATCH 068/177] using global palette --- src/image/loading_displaying.js | 36 +++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 433e2a5769..b550d8de05 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -258,12 +258,13 @@ p5.prototype.saveGif = function(...args) { pixelDensity(1); const pd = this._pixelDensity; + + // width and height based on (p)ixel (d)ensity const width_pd = this.width * pd; const height_pd = this.height * pd; - const gif = GIFEncoder(); - const format = 'rgba4444'; - + // We first take every frame that we are going to use for the animation + let frames = []; while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -276,20 +277,33 @@ p5.prototype.saveGif = function(...args) { const data = this.drawingContext.getImageData(0, 0, width_pd, height_pd) .data; - const palette = quantize(data, 256, { format }); + frames.push(data); + count++; + } + console.info('Frames processed, encoding gif. This may take a while...'); + // create the gif encoder and the colorspace format + const gif = GIFEncoder(); + const format = 'rgba4444'; + + // first generate an optimal palette for the whole animation + const palette = quantize(frames[0], 256, { format }); + for (let i = 0; i < frames.length; i++) { // Apply palette to RGBA data to get an indexed bitmap - const index = applyPalette(data, palette, format); + const index = applyPalette(frames[i], palette, format); - // Write frame into GIF - gif.writeFrame(index, width, height, { palette, delay: 20 }); + // Write frame into the encoder - count++; - } + //if it's the first frame, also add what will be the global palette + if (i === 0) { + gif.writeFrame(index, width_pd, height_pd, { palette, delay: 20 }); + } - console.info('Frames processed, encoding gif. This may take a while...'); + // all subsequent frames will just use the global palette + gif.writeFrame(index, width_pd, height_pd, { delay: 20 }); + } - // gif.finish(); + gif.finish(); loop(); From f46d0eb5b49ea2ca7628a81be55c3000968454db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 20 Jul 2022 16:42:45 +0200 Subject: [PATCH 069/177] made a different global palette generator --- lib/empty-example/sketch.js | 424 ++++++++++++++++++------------------ src/image/image.js | 48 ++-- 2 files changed, 244 insertions(+), 228 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 3f9232f212..5e98625eaa 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,238 +1,238 @@ /* eslint-disable no-unused-vars */ -function setup() { - // put setup code here - createCanvas(200, 200); -} +// function setup() { +// // put setup code here +// createCanvas(300, 300); +// } -function draw() { - // put drawing code here - let hue = map(sin(frameCount / 100), -1, 1, 0, 100); - background(hue); - - line(width / 2, 0, width / 2, height); - line(0, height / 2, width, height / 2); - - fill(250); - circle( - 100 * sin(frameCount / 70) + width / 2, - 100 * sin(frameCount / 70) + height / 2, - 100 - ); -} +// function draw() { +// // put drawing code here +// let hue = map(sin(frameCount / 100), -1, 1, 0, 100); +// background(hue, 30, 100); + +// line(width / 2, 0, width / 2, height); +// line(0, height / 2, width, height / 2); + +// fill(250, hue); +// circle( +// 100 * sin(frameCount / 70) + width / 2, +// 100 * sin(frameCount / 70) + height / 2, +// 100 +// ); +// } -function mousePressed() { - if (mouseButton === RIGHT) { - saveGif('mySketch', 2, 3); - } -} +// function mousePressed() { +// if (mouseButton === RIGHT) { +// saveGif('mySketch', 2, 3); +// } +// } // / COMPLEX SKETCH -// let offset; -// let spacing; +let offset; +let spacing; -// // let font; -// // function preload() { -// // font = loadFont("../SF-Mono-Regular.otf"); -// // } +// let font; +// function preload() { +// font = loadFont("../SF-Mono-Regular.otf"); +// } -// function setup() { -// randomSeed(125); +function setup() { + randomSeed(125); -// w = min(windowHeight, windowWidth); -// createCanvas(w, w); + w = min(windowHeight, windowWidth); + createCanvas(400, 400); -// looping = false; -// saving = false; -// noLoop(); + looping = false; + saving = false; + noLoop(); -// divisor = random(1.2, 3).toFixed(2); + divisor = random(1.2, 3).toFixed(2); -// frameWidth = w / divisor; -// offset = (-frameWidth + w) / 2; + frameWidth = w / divisor; + offset = (-frameWidth + w) / 2; -// gen_num_total_squares = int(random(2, 20)); -// spacing = frameWidth / gen_num_total_squares; + gen_num_total_squares = int(random(2, 20)); + spacing = frameWidth / gen_num_total_squares; -// initHue = random(0, 360); -// compColor = (initHue + 360 / random(1, 4)) % 360; + initHue = random(0, 360); + compColor = (initHue + 360 / random(1, 4)) % 360; -// gen_stroke_weight = random(-100, 100); -// gen_stroke_fade_speed = random(30, 150); -// gen_shift_small_squares = random(0, 10); + gen_stroke_weight = random(-100, 100); + gen_stroke_fade_speed = random(30, 150); + gen_shift_small_squares = random(0, 10); -// gen_offset_small_sq_i = random(3, 10); -// gen_offset_small_sq_j = random(3, 10); + gen_offset_small_sq_i = random(3, 10); + gen_offset_small_sq_j = random(3, 10); -// gen_rotation_speed = random(30, 250); + gen_rotation_speed = random(30, 250); -// gen_depth = random(5, 20); -// gen_offset_i = random(1, 10); -// gen_offset_j = random(1, 10); + gen_depth = random(5, 20); + gen_offset_i = random(1, 10); + gen_offset_j = random(1, 10); -// gen_transparency = random(20, 255); + gen_transparency = random(20, 255); -// background(24); -// } + background(24); +} -// function draw() { -// colorMode(HSB); -// background(initHue, 80, 20, gen_transparency); -// makeSquares(); -// // addHandle(); +function draw() { + colorMode(HSB); + background(initHue, 80, 20, gen_transparency); + makeSquares(); + // addHandle(); -// if (saving) save('grid' + frameCount + '.png'); -// } + if (saving) save('grid' + frameCount + '.png'); +} -// function makeSquares(depth = gen_depth) { -// colorMode(HSB); -// let count_i = 0; - -// for (let i = offset; i < w - offset; i += spacing) { -// let count_j = 0; -// count_i++; - -// if (count_i > gen_num_total_squares) break; - -// for (let j = offset; j < w - offset; j += spacing) { -// count_j++; - -// if (count_j > gen_num_total_squares) break; - -// for (let n = 0; n < depth; n++) { -// noFill(); - -// if (n === 0) { -// stroke(initHue, 100, 100); -// fill( -// initHue, -// 100, -// 100, -// map( -// sin( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 0.3 -// ) -// ); -// } else { -// stroke(compColor, map(n, 0, depth, 100, 0), 100); -// fill( -// compColor, -// 100, -// 100, -// map( -// cos( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 0.3 -// ) -// ); -// } - -// strokeWeight( -// map( -// sin( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 1.5 -// ) -// ); - -// push(); -// translate(i + spacing / 2, j + spacing / 2); - -// rotate( -// i * gen_offset_i + -// j * gen_offset_j + -// frameCount / (gen_rotation_speed / (n + 1)) -// ); - -// if (n % 2 !== 0) { -// translate( -// sin(frameCount / 50) * gen_shift_small_squares, -// cos(frameCount / 50) * gen_shift_small_squares -// ); -// rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); -// } - -// if (n > 0) -// rect( -// -spacing / (gen_offset_small_sq_i + n), -// -spacing / (gen_offset_small_sq_j + n), -// spacing / (n + 1), -// spacing / (n + 1) -// ); -// else rect(-spacing / 2, -spacing / 2, spacing, spacing); - -// pop(); -// } -// // strokeWeight(40); -// // point(i, j); -// } -// } -// } +function makeSquares(depth = gen_depth) { + colorMode(HSB); + let count_i = 0; + + for (let i = offset; i < w - offset; i += spacing) { + let count_j = 0; + count_i++; + + if (count_i > gen_num_total_squares) break; + + for (let j = offset; j < w - offset; j += spacing) { + count_j++; + + if (count_j > gen_num_total_squares) break; + + for (let n = 0; n < depth; n++) { + noFill(); + + if (n === 0) { + stroke(initHue, 100, 100); + fill( + initHue, + 100, + 100, + map( + sin( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 0.3 + ) + ); + } else { + stroke(compColor, map(n, 0, depth, 100, 0), 100); + fill( + compColor, + 100, + 100, + map( + cos( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 0.3 + ) + ); + } + + strokeWeight( + map( + sin( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 1.5 + ) + ); + + push(); + translate(i + spacing / 2, j + spacing / 2); + + rotate( + i * gen_offset_i + + j * gen_offset_j + + frameCount / (gen_rotation_speed / (n + 1)) + ); + + if (n % 2 !== 0) { + translate( + sin(frameCount / 50) * gen_shift_small_squares, + cos(frameCount / 50) * gen_shift_small_squares + ); + rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); + } + + if (n > 0) + rect( + -spacing / (gen_offset_small_sq_i + n), + -spacing / (gen_offset_small_sq_j + n), + spacing / (n + 1), + spacing / (n + 1) + ); + else rect(-spacing / 2, -spacing / 2, spacing, spacing); + + pop(); + } + // strokeWeight(40); + // point(i, j); + } + } +} -// function addHandle() { -// fill(40); -// noStroke(); -// textAlign(RIGHT, BOTTOM); -// textFont(font); -// textSize(20); -// text('@jesi_rgb', w - 30, w - 30); -// } +function addHandle() { + fill(40); + noStroke(); + textAlign(RIGHT, BOTTOM); + textFont(font); + textSize(20); + text('@jesi_rgb', w - 30, w - 30); +} -// function mousePressed() { -// if (mouseButton === LEFT) { -// if (looping) { -// noLoop(); -// looping = false; -// } else { -// loop(); -// looping = true; -// } -// } -// } +function mousePressed() { + if (mouseButton === LEFT) { + if (looping) { + noLoop(); + looping = false; + } else { + loop(); + looping = true; + } + } +} -// function keyPressed() { -// console.log(key); -// switch (key) { -// // pressing the 's' key -// case 's': -// saveGif('mySketch', 2); -// break; - -// // pressing the '0' key -// case '0': -// frameCount = 0; -// loop(); -// noLoop(); -// break; - -// // pressing the ← key -// case 'ArrowLeft': -// frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); -// noLoop(); -// console.log(frameCount); -// break; - -// // pressing the → key -// case 'ArrowRights': -// frameCount += 1; -// noLoop(); -// console.log(frameCount); -// break; - -// default: -// break; -// } -// } +function keyPressed() { + console.log(key); + switch (key) { + // pressing the 's' key + case 's': + saveGif('mySketch', 2); + break; + + // pressing the '0' key + case '0': + frameCount = 0; + loop(); + noLoop(); + break; + + // pressing the ← key + case 'ArrowLeft': + frameCount > 0 ? (frameCount -= 1) : (frameCount = 0); + noLoop(); + console.log(frameCount); + break; + + // pressing the → key + case 'ArrowRights': + frameCount += 1; + noLoop(); + console.log(frameCount); + break; + + default: + break; + } +} diff --git a/src/image/image.js b/src/image/image.js index 270b80a1f5..32086020e9 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -197,6 +197,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const buffer = new Uint8Array(pImg.width * pImg.height * props.numFrames); const allFramesPixelColors = []; + const allColorsFreq = {}; // Used to determine the occurrence of unique palettes and the frames // which use them @@ -219,6 +220,12 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { // What color does this pixel have in this frame ? pixelColors[k] = color; + + if (allColorsFreq[color] === undefined) { + allColorsFreq[color] = { count: 1 }; + } else { + allColorsFreq[color].count += 1; + } } // A way to put use the entire palette as an object key @@ -244,14 +251,18 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { return paletteFreqsAndFrames[b].freq - paletteFreqsAndFrames[a].freq; }); - // The initial global palette is the one with the most occurrence - const globalPalette = palettesSortedByFreq[0] - .split(',') + const allColorsSortedByFreq = Object.keys(allColorsFreq).sort((a, b) => { + return allColorsFreq[b].count - allColorsFreq[a].count; + }); + + // Take the top 256 colors + const globalPalette = allColorsSortedByFreq + .slice(0, 256) .map(a => parseInt(a)); - framesUsingGlobalPalette = framesUsingGlobalPalette.concat( - paletteFreqsAndFrames[globalPalette].frames - ); + // framesUsingGlobalPalette = framesUsingGlobalPalette.concat( + // paletteFreqsAndFrames[globalPalette].frames + // ); const globalPaletteSet = new Set(globalPalette); @@ -310,7 +321,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { // transparent. We decide one particular color as transparent and make all // transparent pixels take this color. This helps in later in compression. for (let i = 0; i < props.numFrames; i++) { - const localPaletteRequired = !framesUsingGlobalPalette.has(i); + const localPaletteRequired = false; const palette = localPaletteRequired ? [] : globalPalette; const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height); @@ -324,7 +335,7 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const color = allFramesPixelColors[i][k]; if (localPaletteRequired) { // local palette cannot be greater than 256 colors - if (colorIndicesLookup[color] === undefined && palette.length <= 255) { + if (colorIndicesLookup[color] === undefined && palette.length < 256) { colorIndicesLookup[color] = palette.length; palette.push(color); } @@ -367,15 +378,20 @@ p5.prototype.encodeAndDownloadGif = function(pImg, filename) { } frameOpts.delay = props.frames[i].delay / 10; // Move timing back into GIF formatting - if (localPaletteRequired) { - // force palette to be power of 2 - let powof2 = 1; - while (powof2 < palette.length) { - powof2 <<= 1; - } - palette.length = constrain(powof2, 2, 256); - frameOpts.palette = new Uint32Array(palette); + // write the global palette with the first frame. + // subsequent frames will also use the global palette + if (i === 0) { + frameOpts.palette = new Uint32Array(globalPalette); } + // if (localPaletteRequired) { + // // force palette to be power of 2 + // let powof2 = 1; + // while (powof2 < palette.length) { + // powof2 <<= 1; + // } + // palette.length = constrain(powof2, 2, 256); + // frameOpts.palette = new Uint32Array(palette); + // } if (i > 0) { gifWriter.addFrame( 0, From f775062ecf02e095bc330807c3fdcf92f7c80f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 20 Jul 2022 18:10:37 +0200 Subject: [PATCH 070/177] creating a global color palette --- src/image/loading_displaying.js | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index b550d8de05..0d6c633eed 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -287,16 +287,41 @@ p5.prototype.saveGif = function(...args) { const format = 'rgba4444'; // first generate an optimal palette for the whole animation - const palette = quantize(frames[0], 256, { format }); + + let colorFreq = {}; + for (let f of frames) { + let currPalette = quantize(f, 256, { format }); + for (let color of currPalette) { + color = color.toString(); + if (colorFreq[color] === undefined) { + colorFreq[color] = { count: 1 }; + } else { + colorFreq[color].count += 1; + } + } + } + + let colorsSortedByFreq = Object.keys(colorFreq) + .sort((a, b) => { + return colorFreq[b].count - colorFreq[a].count; + }) + .map(c => c.split(',').map(x => parseInt(x))); + + const globalPalette = colorsSortedByFreq.splice(0, 256); + print(globalPalette); + for (let i = 0; i < frames.length; i++) { // Apply palette to RGBA data to get an indexed bitmap - const index = applyPalette(frames[i], palette, format); + const index = applyPalette(frames[i], globalPalette, format); // Write frame into the encoder //if it's the first frame, also add what will be the global palette if (i === 0) { - gif.writeFrame(index, width_pd, height_pd, { palette, delay: 20 }); + gif.writeFrame(index, width_pd, height_pd, { + palette: globalPalette, + delay: 20 + }); } // all subsequent frames will just use the global palette From 911d768db8a1deff8b83a35e2b55cc4bd176991a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:51:45 +0000 Subject: [PATCH 071/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5aa27edab9..4f8f8df5de 100644 --- a/README.md +++ b/README.md @@ -574,6 +574,7 @@ We recognize all types of contributions. This project follows the [all-contribut
LEMIBANDDEXARI

🌍
Vivek Tiwari

🌍
Kevin Grajeda

💻 +
anniezhengg

💻 🎨 From db846689281de2da9e68a12ddcf437611bc7ac94 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:51:46 +0000 Subject: [PATCH 072/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8513335ced..659b5be376 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3148,6 +3148,16 @@ "contributions": [ "code" ] + }, + { + "login": "anniezhengg", + "name": "anniezhengg", + "avatar_url": "https://avatars.githubusercontent.com/u/78184655?v=4", + "profile": "https://github.com/anniezhengg", + "contributions": [ + "code", + "design" + ] } ], "repoType": "github", From 781098a25fc5926f9dc4c56a917322a862e02de7 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Wed, 20 Jul 2022 12:33:48 -0700 Subject: [PATCH 073/177] Add more stewards --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f8f8df5de..164461c3ca 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese), [@aahdee](https://github.com/aahdee) | | [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth), [@davepagurek](https://github.com/davepagurek), [@jeffawang](https://github.com/jeffawang) | | [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken) | -| [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye) | +| [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye), [@SamirDhoke](https://github.com/SamirDhoke) | | [Events](https://github.com/processing/p5.js/tree/main/src/events) | [@limzykenneth](https://github.com/limzykenneth) | | [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko), [@KleoP](https://github.com/KleoP) | [IO](https://github.com/processing/p5.js/tree/main/src/io) | [@limzykenneth](https://github.com/limzykenneth) | From d2491d90bf3981fc4b7399007c6d433105c6e4fd Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 02:00:17 +0000 Subject: [PATCH 074/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 164461c3ca..84f5fec034 100644 --- a/README.md +++ b/README.md @@ -575,6 +575,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Vivek Tiwari

🌍
Kevin Grajeda

💻
anniezhengg

💻 🎨 +
Seung-Gi Kim(David)

🌍 From 1b65b3bb22eb3e49b1f201ed7540c87c34d9b03e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 02:00:18 +0000 Subject: [PATCH 075/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 659b5be376..c8f6e2869a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3158,6 +3158,15 @@ "code", "design" ] + }, + { + "login": "SNP0301", + "name": "Seung-Gi Kim(David)", + "avatar_url": "https://avatars.githubusercontent.com/u/68281918?v=4", + "profile": "https://github.com/SNP0301", + "contributions": [ + "translation" + ] } ], "repoType": "github", From f9e4735d86056b3a203b920a2b72dcc6cbc2391c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 21 Jul 2022 12:57:32 +0200 Subject: [PATCH 076/177] fancier palette generation and manipulation --- src/image/loading_displaying.js | 124 +++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 27 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 0d6c633eed..bc96c83c71 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -208,7 +208,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * @alt * animation of a circle moving smoothly diagonally */ -p5.prototype.saveGif = function(...args) { +p5.prototype.saveGif = async function(...args) { // process args let fileName; @@ -286,46 +286,69 @@ p5.prototype.saveGif = function(...args) { const gif = GIFEncoder(); const format = 'rgba4444'; - // first generate an optimal palette for the whole animation + // calculate the global palette for this set of frames + const globalPalette = generateGlobalPalette(frames, format); - let colorFreq = {}; - for (let f of frames) { - let currPalette = quantize(f, 256, { format }); - for (let color of currPalette) { - color = color.toString(); - if (colorFreq[color] === undefined) { - colorFreq[color] = { count: 1 }; - } else { - colorFreq[color].count += 1; + // we are going to iterate the frames in pairs, n-1 and n + for (let i = 0; i < frames.length; i++) { + let currFramePixels = frames[i]; + let lastFramePixels = frames[i - 1]; + + //matching pixels between frames can be set to full transparency, + // kinda digging a "hole" into the frame to see the pixels that where behind it + // (which would be the exact same, so not noticeable changes) + // this helps make the file smaller + let matchingPixelsInFrames = []; + if (i > 0) { + for (let p = 0; p < currFramePixels.length; p += 4) { + let currPixel = [ + currFramePixels[p], + currFramePixels[p + 1], + currFramePixels[p + 2], + currFramePixels[p + 3] + ]; + let lastPixel = [ + lastFramePixels[p], + lastFramePixels[p + 1], + lastFramePixels[p + 2], + lastFramePixels[p + 3] + ]; + if (pixelEquals(currPixel, lastPixel)) { + matchingPixelsInFrames.push(parseInt(p / 4)); + } } } - } - let colorsSortedByFreq = Object.keys(colorFreq) - .sort((a, b) => { - return colorFreq[b].count - colorFreq[a].count; - }) - .map(c => c.split(',').map(x => parseInt(x))); - - const globalPalette = colorsSortedByFreq.splice(0, 256); - print(globalPalette); - - for (let i = 0; i < frames.length; i++) { + // we decide on one of this colors to be fully transparent + const transparentIndex = matchingPixelsInFrames[0]; // Apply palette to RGBA data to get an indexed bitmap - const index = applyPalette(frames[i], globalPalette, format); + const indexedFrame = applyPalette(frames[i], globalPalette, { format }); + + for (let mp = 0; mp < matchingPixelsInFrames.length; mp++) { + let samePixelIndex = matchingPixelsInFrames[mp]; + indexedFrame[samePixelIndex] = transparentIndex; + } // Write frame into the encoder - //if it's the first frame, also add what will be the global palette + // if it's the first frame, also add what will be the global palette if (i === 0) { - gif.writeFrame(index, width_pd, height_pd, { + gif.writeFrame(indexedFrame, width_pd, height_pd, { palette: globalPalette, - delay: 20 + delay: 20, + dispose: 1 }); } // all subsequent frames will just use the global palette - gif.writeFrame(index, width_pd, height_pd, { delay: 20 }); + gif.writeFrame(indexedFrame, width_pd, height_pd, { + delay: 20, + transparent: true, + transparentIndex: transparentIndex, + dispose: 1 + }); + + await new Promise(resolve => setTimeout(resolve, 0)); } gif.finish(); @@ -342,6 +365,53 @@ p5.prototype.saveGif = function(...args) { p5.prototype.downloadFile(blob, fileName, extension); }; +function generateGlobalPalette(frames, format) { + // for each frame, we'll keep track of the count of + // every unique color. that is: how many times does + // this particular color appear in every frame? + // Then we'll sort the colors and pick the top 256! + + // calculate the frequency table for the colors + let colorFreq = {}; + for (let f of frames) { + let currPalette = quantize(f, 256, { format }); + for (let color of currPalette) { + // colors are in the format [r, g, b, (a)], as in [255, 127, 45, 255] + // we'll convert the array to its string representation so it can be used as an index! + color = color.toString(); + if (colorFreq[color] === undefined) { + colorFreq[color] = { count: 1 }; + } else { + colorFreq[color].count += 1; + } + } + } + + // at this point colorFreq is a dict with {color: count}, + // telling us how many times each color appears in the whole animation + + // we process it undoing the string operation coverting that into + // an array of strings (['255', '127', '45', '255']) and then we convert + // that again to an array of integers + let colorsSortedByFreq = Object.keys(colorFreq) + .sort((a, b) => { + return colorFreq[b].count - colorFreq[a].count; + }) + .map(c => c.split(',').map(x => parseInt(x))); + + // now we simply extract the top 256 colors! + return colorsSortedByFreq.splice(0, 256); +} + +function pixelEquals(a, b) { + return ( + Array.isArray(a) && + Array.isArray(b) && + a.length === b.length && + a.every((val, index) => val === b[index]) + ); +} + /** * Helper function for loading GIF-based images */ From 01f393939348718c82d91f7a5276078027c600ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 21 Jul 2022 13:45:10 +0200 Subject: [PATCH 077/177] debuggnig and restructuring --- lib/empty-example/sketch.js | 2 +- src/image/loading_displaying.js | 62 ++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index fa859fe8e7..d85d3b4d95 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -220,7 +220,7 @@ function keyPressed() { switch (key) { // pressing the 's' key case 's': - saveGif('mySketch', 3); + saveGif('mySketch', 1); break; // pressing the '0' key diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index bc96c83c71..516bea9857 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -291,15 +291,24 @@ p5.prototype.saveGif = async function(...args) { // we are going to iterate the frames in pairs, n-1 and n for (let i = 0; i < frames.length; i++) { - let currFramePixels = frames[i]; - let lastFramePixels = frames[i - 1]; + if (i === 0) { + const indexedFrame = applyPalette(frames[i], globalPalette, { format }); + gif.writeFrame(indexedFrame, width_pd, height_pd, { + palette: globalPalette, + delay: 20, + dispose: 1 + }); + continue; + } - //matching pixels between frames can be set to full transparency, + // matching pixels between frames can be set to full transparency, // kinda digging a "hole" into the frame to see the pixels that where behind it // (which would be the exact same, so not noticeable changes) // this helps make the file smaller - let matchingPixelsInFrames = []; if (i > 0) { + let currFramePixels = frames[i]; + let lastFramePixels = frames[i - 1]; + let matchingPixelsInFrames = []; for (let p = 0; p < currFramePixels.length; p += 4) { let currPixel = [ currFramePixels[p], @@ -317,37 +326,34 @@ p5.prototype.saveGif = async function(...args) { matchingPixelsInFrames.push(parseInt(p / 4)); } } - } - - // we decide on one of this colors to be fully transparent - const transparentIndex = matchingPixelsInFrames[0]; - // Apply palette to RGBA data to get an indexed bitmap - const indexedFrame = applyPalette(frames[i], globalPalette, { format }); - - for (let mp = 0; mp < matchingPixelsInFrames.length; mp++) { - let samePixelIndex = matchingPixelsInFrames[mp]; - indexedFrame[samePixelIndex] = transparentIndex; - } - - // Write frame into the encoder + // we decide on one of this colors to be fully transparent + const transparentIndex = matchingPixelsInFrames[0]; + // Apply palette to RGBA data to get an indexed bitmap + const indexedFrame = applyPalette(frames[i], globalPalette, { format }); + + for (let mp = 0; mp < matchingPixelsInFrames.length; mp++) { + let samePixelIndex = matchingPixelsInFrames[mp]; + // here, we overwrite whatever color this pixel was assigned to + // with the color that we decided we are going to use as transparent. + // down in writeFrame we are going to tell the encoder that whenever + // it runs into "transparentIndex", just dig a hole there allowing to + // see through what was in the frame before it. + indexedFrame[samePixelIndex] = transparentIndex; + } + // Write frame into the encoder + // if it's the first frame, also add what will be the global palette - // if it's the first frame, also add what will be the global palette - if (i === 0) { + // all subsequent frames will just use the global palette gif.writeFrame(indexedFrame, width_pd, height_pd, { - palette: globalPalette, delay: 20, + transparent: true, + transparentIndex: transparentIndex, dispose: 1 }); } - // all subsequent frames will just use the global palette - gif.writeFrame(indexedFrame, width_pd, height_pd, { - delay: 20, - transparent: true, - transparentIndex: transparentIndex, - dispose: 1 - }); - + // this just makes the process asynchronous, preventing + // that the encoding locks up the browser await new Promise(resolve => setTimeout(resolve, 0)); } From 88ee5225a5a3656663613b396259663b17a011b2 Mon Sep 17 00:00:00 2001 From: stampyzfanz <34364128+stampyzfanz@users.noreply.github.com> Date: Thu, 21 Jul 2022 21:57:17 +1000 Subject: [PATCH 078/177] Add automatic labelling to issues --- .github/labeler.yml | 44 +++++++++++++++++++++++++---------- .github/workflows/labeler.yml | 14 +++++++++++ 2 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml index 70b33005f7..08faf5e42e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,12 +1,32 @@ -# Number of labels to fetch (optional). Defaults to 20 -numLabels: 20 -# These labels will not be used even if the issue contains them -excludeLabels: - - bug - - can't reproduce - - known issue - - more info needed - - will not fix - - severity:critical - - severity:major - - severity:minor +"area:Accessibility": + - '\[X\] Accessibility \(Web Accessibility\)' +"area:Build Process": + - '\[X\] Build tools and processes' +"area:Color": + - '\[X\] Color' +"area:Core/Environment/Rendering": + - '\[X\] Core' +"area:Data": + - '\[X\] Data' +"area:DOM": + - '\[X\] DOM' +"area:Events": + - '\[X\] Events' +"area:Friendly-Errors": + - '\[X\] Friendly error system' +"area:Image": + - '\[X\] Image' +"area:IO": + - '\[X\] IO \(Input/Output\)' +"Localization": + - '\[X\] Localization' +"area:Math": + - '\[X\] Math' +"area:Unit Testing": + - '\[X\] Unit Testing' +"area:Typography": + - '\[X\] Typography' +"area:Utilities": + - '\[X\] Utilities' +"area:WebGL": + - '\[X\] WebGL' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..f5ae4b407b --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,14 @@ +name: "Issue Labeler" +on: + issues: + types: [opened, edited] + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: github/issue-labeler@v2.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler.yml + enable-versioned-regex: 0 From 7aa8b9c38d78d2fd88d3815cdf901e76f6780ebd Mon Sep 17 00:00:00 2001 From: stampyzfanz <34364128+stampyzfanz@users.noreply.github.com> Date: Thu, 21 Jul 2022 22:08:46 +1000 Subject: [PATCH 079/177] Fix incorrect label names --- .github/labeler.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 08faf5e42e..4dcd4e5098 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,10 +1,10 @@ "area:Accessibility": - '\[X\] Accessibility \(Web Accessibility\)' -"area:Build Process": +"Build Process": - '\[X\] Build tools and processes' "area:Color": - '\[X\] Color' -"area:Core/Environment/Rendering": +"area:Core": - '\[X\] Core' "area:Data": - '\[X\] Data' @@ -12,7 +12,7 @@ - '\[X\] DOM' "area:Events": - '\[X\] Events' -"area:Friendly-Errors": +"Friendly-Errors": - '\[X\] Friendly error system' "area:Image": - '\[X\] Image' @@ -22,7 +22,7 @@ - '\[X\] Localization' "area:Math": - '\[X\] Math' -"area:Unit Testing": +"Unit Testing": - '\[X\] Unit Testing' "area:Typography": - '\[X\] Typography' From 9cd186349cdb55c5faf28befff9c0d4a390e02ed Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 21 Jul 2022 15:40:18 -0700 Subject: [PATCH 080/177] 1.4.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ffff484561..fec35c0586 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "p5", - "version": "1.4.1", + "version": "1.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 76acf7cb9b..4c4cba9eb6 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "node --require @babel/register ./utils/sample-linter.js" ] }, - "version": "1.4.1", + "version": "1.4.2", "devDependencies": { "@babel/core": "^7.7.7", "@babel/preset-env": "^7.10.2", From 064fac0e947228fabedfe6c267a581d50c359ed4 Mon Sep 17 00:00:00 2001 From: stampyzfanz <34364128+stampyzfanz@users.noreply.github.com> Date: Fri, 22 Jul 2022 09:26:48 +1000 Subject: [PATCH 081/177] Update label names, reorder issue form checkboxes --- .../existing-feature-enhancement.yml | 68 +++++++++---------- .github/ISSUE_TEMPLATE/feature-request.yml | 68 +++++++++---------- .github/ISSUE_TEMPLATE/found-a-bug.yml | 34 +++++----- .github/labeler.yml | 40 +++++------ 4 files changed, 105 insertions(+), 105 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml index 60d4e05ada..1b9a3f74b0 100644 --- a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml @@ -2,37 +2,37 @@ name: 💡 Existing Feature Enhancement description: This template is for suggesting an improvement for an existing feature. labels: [enhancement] body: -- type: textarea - attributes: - label: Increasing Access - description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) - validations: - required: true -- type: checkboxes - id: sub-area - attributes: - label: Most appropriate sub-area of p5.js? - description: You may select more than one. - options: - - label: Accessibility (Web Accessibility) - - label: Build tools and processes - - label: Color - - label: Core/Environment/Rendering - - label: Data - - label: DOM - - label: Events - - label: Friendly error system - - label: Image - - label: IO (Input/Output) - - label: Localization - - label: Math - - label: Unit Testing - - label: Typography - - label: Utilities - - label: WebGL - - label: Other (specify if possible) -- type: textarea - attributes: - label: Feature enhancement details - validations: - required: true + - type: textarea + attributes: + label: Increasing Access + description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) + validations: + required: true + - type: checkboxes + id: sub-area + attributes: + label: Most appropriate sub-area of p5.js? + description: You may select more than one. + options: + - label: Accessibility (Web Accessibility) + - label: Color + - label: Core/Environment/Rendering + - label: Data + - label: DOM + - label: Events + - label: Image + - label: IO (Input/Output) + - label: Math + - label: Typography + - label: Utilities + - label: WebGL + - label: Build tools and processes + - label: Unit Testing + - label: Friendly error system + - label: Localization + - label: Other (specify if possible) + - type: textarea + attributes: + label: Feature enhancement details + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 88f7531899..46de3c6cdd 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -2,37 +2,37 @@ name: 🌱 New Feature Request description: This template is for requesting a new feature be added. labels: [feature request] body: -- type: textarea - attributes: - label: Increasing Access - description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) - validations: - required: true -- type: checkboxes - id: sub-area - attributes: - label: Most appropriate sub-area of p5.js? - description: You may select more than one. - options: - - label: Accessibility (Web Accessibility) - - label: Build tools and processes - - label: Color - - label: Core/Environment/Rendering - - label: Data - - label: DOM - - label: Events - - label: Friendly error system - - label: Image - - label: IO (Input/Output) - - label: Localization - - label: Math - - label: Unit Testing - - label: Typography - - label: Utilities - - label: WebGL - - label: Other (specify if possible) -- type: textarea - attributes: - label: Feature request details - validations: - required: true \ No newline at end of file + - type: textarea + attributes: + label: Increasing Access + description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) + validations: + required: true + - type: checkboxes + id: sub-area + attributes: + label: Most appropriate sub-area of p5.js? + description: You may select more than one. + options: + - label: Accessibility (Web Accessibility) + - label: Color + - label: Core/Environment/Rendering + - label: Data + - label: DOM + - label: Events + - label: Image + - label: IO (Input/Output) + - label: Math + - label: Typography + - label: Utilities + - label: WebGL + - label: Build tools and processes + - label: Unit Testing + - label: Friendly error system + - label: Localization + - label: Other (specify if possible) + - type: textarea + attributes: + label: Feature request details + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/found-a-bug.yml b/.github/ISSUE_TEMPLATE/found-a-bug.yml index 1286249b6b..d2f6899db9 100644 --- a/.github/ISSUE_TEMPLATE/found-a-bug.yml +++ b/.github/ISSUE_TEMPLATE/found-a-bug.yml @@ -8,23 +8,23 @@ body: label: Most appropriate sub-area of p5.js? description: You may select more than one. options: - - label: Accessibility (Web Accessibility) - - label: Build tools and processes - - label: Color - - label: Core/Environment/Rendering - - label: Data - - label: DOM - - label: Events - - label: Friendly error system - - label: Image - - label: IO (Input/Output) - - label: Localization - - label: Math - - label: Unit Testing - - label: Typography - - label: Utilities - - label: WebGL - - label: Other (specify if possible) + - label: Accessibility (Web Accessibility) + - label: Color + - label: Core/Environment/Rendering + - label: Data + - label: DOM + - label: Events + - label: Image + - label: IO (Input/Output) + - label: Math + - label: Typography + - label: Utilities + - label: WebGL + - label: Build tools and processes + - label: Unit Testing + - label: Friendly error system + - label: Localization + - label: Other (specify if possible) - type: input attributes: label: p5.js version diff --git a/.github/labeler.yml b/.github/labeler.yml index 4dcd4e5098..3274b9bd6d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,32 +1,32 @@ -"area:Accessibility": +"Area:Accessibility": - '\[X\] Accessibility \(Web Accessibility\)' -"Build Process": - - '\[X\] Build tools and processes' -"area:Color": +"Area:Color": - '\[X\] Color' -"area:Core": +"Area:Core": - '\[X\] Core' -"area:Data": +"Area:Data": - '\[X\] Data' -"area:DOM": +"Area:DOM": - '\[X\] DOM' -"area:Events": +"Area:Events": - '\[X\] Events' -"Friendly-Errors": - - '\[X\] Friendly error system' -"area:Image": +"Area:Image": - '\[X\] Image' -"area:IO": +"Area:IO": - '\[X\] IO \(Input/Output\)' -"Localization": - - '\[X\] Localization' -"area:Math": +"Area:Math": - '\[X\] Math' -"Unit Testing": - - '\[X\] Unit Testing' -"area:Typography": +"Area:Typography": - '\[X\] Typography' -"area:Utilities": +"Area:Utilities": - '\[X\] Utilities' -"area:WebGL": +"Area:WebGL": - '\[X\] WebGL' +"Build Process": + - '\[X\] Build tools and processes' +"Unit Testing": + - '\[X\] Unit Testing' +"Localization": + - '\[X\] Localization' +"Friendly Errors": + - '\[X\] Friendly error system' From f932352533264e815e65704c6c35399ad7270311 Mon Sep 17 00:00:00 2001 From: evelyn masso Date: Thu, 21 Jul 2022 16:47:49 -0700 Subject: [PATCH 082/177] only include necessary files in zip --- Gruntfile.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2953b17d17..1d266f39ac 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -365,7 +365,19 @@ module.exports = grunt => { options: { archive: 'release/p5.zip' }, - files: [{ cwd: 'lib/', src: ['**/*'], expand: true }] + files: [ + { + cwd: 'lib/', + src: [ + 'p5.js', + 'p5.min.js', + 'addons/*', + 'empty-example/*', + 'README.txt' + ], + expand: true + } + ] } }, From 0326f85fbd01df4a7e5760fff180a36a75e0234d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Fri, 22 Jul 2022 19:08:07 +0200 Subject: [PATCH 083/177] minor changes --- lib/empty-example/sketch.js | 467 +++++++++++++++++--------------- src/image/loading_displaying.js | 5 +- 2 files changed, 245 insertions(+), 227 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index d85d3b4d95..726ecfc430 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,250 +1,267 @@ /* eslint-disable no-unused-vars */ -// function setup() { -// // put setup code here -// createCanvas(500, 500); -// } - -// function draw() { -// // put drawing code here -// let hue = map(sin(frameCount / 10), -1, 1, 127, 255); -// let hue_2 = map(sin(frameCount / 100) + 0.791, -1, 1, 127, 255); - -// strokeWeight(0); -// line(width / 2, 0, width / 2, height); -// line(0, height / 2, width, height / 2); - -// fill(40, 40, hue); -// rect(0, 0, width / 2, height / 2); - -// // fill(80, 80, hue); -// rect(width / 2, 0, width / 2, height / 2); - -// fill(0, 180, hue); -// rect(0, height / 2, width / 2, height / 2); - -// // fill(240, 240, 0); -// rect(width / 2, height / 2, width / 2, height / 2); - -// fill(250); -// stroke(250, 250, 20); -// strokeWeight(4); -// circle( -// 100 * sin(frameCount / 20) + width / 2, -// // 100 * sin(frameCount / 20) + height / 2, -// // width / 2, -// height / 2, -// 100 -// ); -// } +let saving = false; +function setup() { + // put setup code here + createCanvas(500, 500); +} -// function mousePressed() { -// if (mouseButton === RIGHT) { -// saveGif('mySketch', 10, 3); -// } -// } +function draw() { + // put drawing code here + let hue = map(sin(frameCount), -1, 1, 127, 255); + let hue_2 = map(sin(frameCount / 100) + 0.791, -1, 1, 127, 255); + + strokeWeight(0); + line(width / 2, 0, width / 2, height); + line(0, height / 2, width, height / 2); + + fill(250, 250, 20); + rect(0, 0, width / 2, height / 2); + + // fill(80, 80, hue); + rect(width / 2, 0, width / 2, height / 2); + + fill(20, 250, 250); + rect(0, height / 2, width / 2, height / 2); + + // fill(240, 240, 0); + rect(width / 2, height / 2, width / 2, height / 2); + + fill(30); + stroke(20, 250, 20); + strokeWeight(4); + circle( + 100 * sin(frameCount / 20) + width / 2, + // 100 * sin(frameCount / 20) + height / 2, + // width / 2, + height / 2, + 100 + ); + + if (saving) { + save('frame' + frameCount.toString()); + } +} -// / COMPLEX SKETCH -let offset; -let spacing; +function mousePressed() { + if (mouseButton === RIGHT) { + saveGif('mySketch', 1, 3); + } +} -function setup() { - // randomSeed(125); +function keyPressed() { + switch (key) { + case 's': + frameRate(3); + frameCount = 0; + saving = !saving; - w = min(windowHeight, windowWidth); - createCanvas(w, w); - print(w); - looping = false; - saving = false; - noLoop(); + if (!saving) frameRate(60); + break; + } +} - divisor = random(1.2, 3).toFixed(2); +// / COMPLEX SKETCH +// let offset; +// let spacing; - frameWidth = w / divisor; - offset = (-frameWidth + w) / 2; +// function setup() { +// randomSeed(1312); - gen_num_total_squares = int(random(2, 20)); - spacing = frameWidth / gen_num_total_squares; +// w = min(windowHeight, windowWidth); +// createCanvas(w, w); +// print(w); +// looping = false; +// saving = false; +// noLoop(); - initHue = random(0, 360); - compColor = (initHue + 360 / random(1, 4)) % 360; +// divisor = random(1.2, 3).toFixed(2); - gen_stroke_weight = random(-100, 100); - gen_stroke_fade_speed = random(30, 150); - gen_shift_small_squares = random(0, 10); +// frameWidth = w / divisor; +// offset = (-frameWidth + w) / 2; - gen_offset_small_sq_i = random(3, 10); - gen_offset_small_sq_j = random(3, 10); +// gen_num_total_squares = int(random(2, 20)); +// spacing = frameWidth / gen_num_total_squares; - gen_rotation_speed = random(30, 250); +// initHue = random(0, 360); +// compColor = (initHue + 360 / random(1, 4)) % 360; - gen_depth = random(5, 20); - gen_offset_i = random(1, 10); - gen_offset_j = random(1, 10); +// gen_stroke_weight = random(-100, 100); +// gen_stroke_fade_speed = random(30, 150); +// gen_shift_small_squares = random(0, 10); - gen_transparency = random(20, 255); +// gen_offset_small_sq_i = random(3, 10); +// gen_offset_small_sq_j = random(3, 10); - background(24); -} +// gen_rotation_speed = random(30, 250); -function draw() { - colorMode(HSB); - background(initHue, 80, 20, gen_transparency); - makeSquares(); - // addHandle(); +// gen_depth = random(5, 20); +// gen_offset_i = random(1, 10); +// gen_offset_j = random(1, 10); - if (saving) save('grid' + frameCount + '.png'); -} +// gen_transparency = random(20, 255); -function makeSquares(depth = gen_depth) { - colorMode(HSB); - let count_i = 0; - - for (let i = offset; i < w - offset; i += spacing) { - let count_j = 0; - count_i++; - - if (count_i > gen_num_total_squares) break; - - for (let j = offset; j < w - offset; j += spacing) { - count_j++; - - if (count_j > gen_num_total_squares) break; - - for (let n = 0; n < depth; n++) { - noFill(); - - if (n === 0) { - stroke(initHue, 100, 100); - fill( - initHue, - 100, - 100, - map( - sin( - gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed - ), - -1, - 1, - 0, - 0.3 - ) - ); - } else { - stroke(compColor, map(n, 0, depth, 100, 0), 100); - fill( - compColor, - 100, - 100, - map( - cos( - gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed - ), - -1, - 1, - 0, - 0.3 - ) - ); - } - - strokeWeight( - map( - sin( - gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed - ), - -1, - 1, - 0, - 1.5 - ) - ); - - push(); - translate(i + spacing / 2, j + spacing / 2); - - rotate( - i * gen_offset_i + - j * gen_offset_j + - frameCount / (gen_rotation_speed / (n + 1)) - ); - - if (n % 2 !== 0) { - translate( - sin(frameCount / 50) * gen_shift_small_squares, - cos(frameCount / 50) * gen_shift_small_squares - ); - rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); - } - - if (n > 0) - rect( - -spacing / (gen_offset_small_sq_i + n), - -spacing / (gen_offset_small_sq_j + n), - spacing / (n + 1), - spacing / (n + 1) - ); - else rect(-spacing / 2, -spacing / 2, spacing, spacing); - - pop(); - } - // strokeWeight(40); - // point(i, j); - } - } -} - -function addHandle() { - fill(40); - noStroke(); - textAlign(RIGHT, BOTTOM); - textFont(font); - textSize(20); - text('@jesi_rgb', w - 30, w - 30); -} +// background(24); +// } -function mousePressed() { - if (mouseButton === LEFT) { - if (looping) { - noLoop(); - looping = false; - } else { - loop(); - looping = true; - } - } -} +// function draw() { +// colorMode(HSB); +// background(initHue, 80, 20, gen_transparency); +// makeSquares(); +// // addHandle(); -function keyPressed() { - console.log(key); - switch (key) { - // pressing the 's' key - case 's': - saveGif('mySketch', 1); - break; +// if (saving) save('grid' + frameCount + '.png'); +// } - // pressing the '0' key - case '0': - frameCount = 0; - loop(); - noLoop(); - break; +// function makeSquares(depth = gen_depth) { +// colorMode(HSB); +// let count_i = 0; + +// for (let i = offset; i < w - offset; i += spacing) { +// let count_j = 0; +// count_i++; + +// if (count_i > gen_num_total_squares) break; + +// for (let j = offset; j < w - offset; j += spacing) { +// count_j++; + +// if (count_j > gen_num_total_squares) break; + +// for (let n = 0; n < depth; n++) { +// noFill(); + +// if (n === 0) { +// stroke(initHue, 100, 100); +// fill( +// initHue, +// 100, +// 100, +// map( +// sin( +// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed +// ), +// -1, +// 1, +// 0, +// 0.3 +// ) +// ); +// } else { +// stroke(compColor, map(n, 0, depth, 100, 0), 100); +// fill( +// compColor, +// 100, +// 100, +// map( +// cos( +// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed +// ), +// -1, +// 1, +// 0, +// 0.3 +// ) +// ); +// } + +// strokeWeight( +// map( +// sin( +// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed +// ), +// -1, +// 1, +// 0, +// 1.5 +// ) +// ); + +// push(); +// translate(i + spacing / 2, j + spacing / 2); + +// rotate( +// i * gen_offset_i + +// j * gen_offset_j + +// frameCount / (gen_rotation_speed / (n + 1)) +// ); + +// if (n % 2 !== 0) { +// translate( +// sin(frameCount / 50) * gen_shift_small_squares, +// cos(frameCount / 50) * gen_shift_small_squares +// ); +// rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); +// } + +// if (n > 0) +// rect( +// -spacing / (gen_offset_small_sq_i + n), +// -spacing / (gen_offset_small_sq_j + n), +// spacing / (n + 1), +// spacing / (n + 1) +// ); +// else rect(-spacing / 2, -spacing / 2, spacing, spacing); + +// pop(); +// } +// // strokeWeight(40); +// // point(i, j); +// } +// } +// } - // pressing the ← key - case 'ArrowLeft': - frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); - noLoop(); - console.log(frameCount); - break; +// function addHandle() { +// fill(40); +// noStroke(); +// textAlign(RIGHT, BOTTOM); +// textFont(font); +// textSize(20); +// text('@jesi_rgb', w - 30, w - 30); +// } - // pressing the → key - case 'ArrowRights': - frameCount += 1; - noLoop(); - console.log(frameCount); - break; +// function mousePressed() { +// if (mouseButton === LEFT) { +// if (looping) { +// noLoop(); +// looping = false; +// } else { +// loop(); +// looping = true; +// } +// } +// } - default: - break; - } -} +// function keyPressed() { +// console.log(key); +// switch (key) { +// // pressing the 's' key +// case 's': +// saveGif('mySketch', 1); +// break; + +// // pressing the '0' key +// case '0': +// frameCount = 0; +// loop(); +// noLoop(); +// break; + +// // pressing the ← key +// case 'ArrowLeft': +// frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); +// noLoop(); +// console.log(frameCount); +// break; + +// // pressing the → key +// case 'ArrowRights': +// frameCount += 1; +// noLoop(); +// console.log(frameCount); +// break; + +// default: +// break; +// } +// } diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 516bea9857..0265c23741 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -284,7 +284,7 @@ p5.prototype.saveGif = async function(...args) { // create the gif encoder and the colorspace format const gif = GIFEncoder(); - const format = 'rgba4444'; + const format = 'rgb444'; // calculate the global palette for this set of frames const globalPalette = generateGlobalPalette(frames, format); @@ -351,6 +351,7 @@ p5.prototype.saveGif = async function(...args) { dispose: 1 }); } + print('Frame: ' + i.toString()); // this just makes the process asynchronous, preventing // that the encoding locks up the browser @@ -380,7 +381,7 @@ function generateGlobalPalette(frames, format) { // calculate the frequency table for the colors let colorFreq = {}; for (let f of frames) { - let currPalette = quantize(f, 256, { format }); + let currPalette = quantize(f, 1024, { format }); for (let color of currPalette) { // colors are in the format [r, g, b, (a)], as in [255, 127, 45, 255] // we'll convert the array to its string representation so it can be used as an index! From 7256b1cd9d0aa655e5cb03b1723bf5dd2ca1cef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sat, 23 Jul 2022 09:32:01 +0200 Subject: [PATCH 084/177] solve bug with transparentIndex --- lib/empty-example/sketch.js | 474 ++++++++++++++++---------------- src/image/loading_displaying.js | 6 +- 2 files changed, 241 insertions(+), 239 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 726ecfc430..4dbb7d70c3 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,267 +1,267 @@ /* eslint-disable no-unused-vars */ -let saving = false; -function setup() { - // put setup code here - createCanvas(500, 500); -} +// let saving = false; +// function setup() { +// // put setup code here +// createCanvas(500, 500); +// } -function draw() { - // put drawing code here - let hue = map(sin(frameCount), -1, 1, 127, 255); - let hue_2 = map(sin(frameCount / 100) + 0.791, -1, 1, 127, 255); - - strokeWeight(0); - line(width / 2, 0, width / 2, height); - line(0, height / 2, width, height / 2); - - fill(250, 250, 20); - rect(0, 0, width / 2, height / 2); - - // fill(80, 80, hue); - rect(width / 2, 0, width / 2, height / 2); - - fill(20, 250, 250); - rect(0, height / 2, width / 2, height / 2); - - // fill(240, 240, 0); - rect(width / 2, height / 2, width / 2, height / 2); - - fill(30); - stroke(20, 250, 20); - strokeWeight(4); - circle( - 100 * sin(frameCount / 20) + width / 2, - // 100 * sin(frameCount / 20) + height / 2, - // width / 2, - height / 2, - 100 - ); - - if (saving) { - save('frame' + frameCount.toString()); - } -} +// function draw() { +// // put drawing code here +// let hue = map(sin(frameCount), -1, 1, 127, 255); +// let hue_2 = map(sin(frameCount / 100) + 0.791, -1, 1, 127, 255); + +// strokeWeight(0); +// line(width / 2, 0, width / 2, height); +// line(0, height / 2, width, height / 2); + +// fill(250, 250, 20); +// rect(0, 0, width / 2, height / 2); + +// // fill(80, 80, hue); +// rect(width / 2, 0, width / 2, height / 2); + +// fill(20, 250, 250); +// rect(0, height / 2, width / 2, height / 2); + +// // fill(240, 240, 0); +// rect(width / 2, height / 2, width / 2, height / 2); + +// fill(30); +// stroke(20, 250, 20); +// strokeWeight(4); +// circle( +// 100 * sin(frameCount / 20) + width / 2, +// // 100 * sin(frameCount / 20) + height / 2, +// // width / 2, +// height / 2, +// 100 +// ); + +// if (saving) { +// save('frame' + frameCount.toString()); +// } +// } -function mousePressed() { - if (mouseButton === RIGHT) { - saveGif('mySketch', 1, 3); - } -} +// function mousePressed() { +// if (mouseButton === RIGHT) { +// saveGif('mySketch', 1, 3); +// } +// } -function keyPressed() { - switch (key) { - case 's': - frameRate(3); - frameCount = 0; - saving = !saving; +// function keyPressed() { +// switch (key) { +// case 's': +// frameRate(3); +// frameCount = 0; +// saving = !saving; - if (!saving) frameRate(60); - break; - } -} +// if (!saving) frameRate(60); +// break; +// } +// } // / COMPLEX SKETCH -// let offset; -// let spacing; +let offset; +let spacing; -// function setup() { -// randomSeed(1312); +function setup() { + // randomSeed(1312); -// w = min(windowHeight, windowWidth); -// createCanvas(w, w); -// print(w); -// looping = false; -// saving = false; -// noLoop(); + w = min(windowHeight, windowWidth); + createCanvas(w, w); + print(w); + looping = false; + saving = false; + noLoop(); -// divisor = random(1.2, 3).toFixed(2); + divisor = random(1.2, 3).toFixed(2); -// frameWidth = w / divisor; -// offset = (-frameWidth + w) / 2; + frameWidth = w / divisor; + offset = (-frameWidth + w) / 2; -// gen_num_total_squares = int(random(2, 20)); -// spacing = frameWidth / gen_num_total_squares; + gen_num_total_squares = int(random(2, 20)); + spacing = frameWidth / gen_num_total_squares; -// initHue = random(0, 360); -// compColor = (initHue + 360 / random(1, 4)) % 360; + initHue = random(0, 360); + compColor = (initHue + 360 / random(1, 4)) % 360; -// gen_stroke_weight = random(-100, 100); -// gen_stroke_fade_speed = random(30, 150); -// gen_shift_small_squares = random(0, 10); + gen_stroke_weight = random(-100, 100); + gen_stroke_fade_speed = random(30, 150); + gen_shift_small_squares = random(0, 10); -// gen_offset_small_sq_i = random(3, 10); -// gen_offset_small_sq_j = random(3, 10); + gen_offset_small_sq_i = random(3, 10); + gen_offset_small_sq_j = random(3, 10); -// gen_rotation_speed = random(30, 250); + gen_rotation_speed = random(30, 250); -// gen_depth = random(5, 20); -// gen_offset_i = random(1, 10); -// gen_offset_j = random(1, 10); + gen_depth = random(5, 20); + gen_offset_i = random(1, 10); + gen_offset_j = random(1, 10); -// gen_transparency = random(20, 255); + gen_transparency = random(20, 255); -// background(24); -// } + background(24); +} -// function draw() { -// colorMode(HSB); -// background(initHue, 80, 20, gen_transparency); -// makeSquares(); -// // addHandle(); +function draw() { + colorMode(HSB); + background(initHue, 80, 20, gen_transparency); + makeSquares(); + // addHandle(); -// if (saving) save('grid' + frameCount + '.png'); -// } + if (saving) save('grid' + frameCount + '.png'); +} -// function makeSquares(depth = gen_depth) { -// colorMode(HSB); -// let count_i = 0; - -// for (let i = offset; i < w - offset; i += spacing) { -// let count_j = 0; -// count_i++; - -// if (count_i > gen_num_total_squares) break; - -// for (let j = offset; j < w - offset; j += spacing) { -// count_j++; - -// if (count_j > gen_num_total_squares) break; - -// for (let n = 0; n < depth; n++) { -// noFill(); - -// if (n === 0) { -// stroke(initHue, 100, 100); -// fill( -// initHue, -// 100, -// 100, -// map( -// sin( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 0.3 -// ) -// ); -// } else { -// stroke(compColor, map(n, 0, depth, 100, 0), 100); -// fill( -// compColor, -// 100, -// 100, -// map( -// cos( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 0.3 -// ) -// ); -// } - -// strokeWeight( -// map( -// sin( -// gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed -// ), -// -1, -// 1, -// 0, -// 1.5 -// ) -// ); - -// push(); -// translate(i + spacing / 2, j + spacing / 2); - -// rotate( -// i * gen_offset_i + -// j * gen_offset_j + -// frameCount / (gen_rotation_speed / (n + 1)) -// ); - -// if (n % 2 !== 0) { -// translate( -// sin(frameCount / 50) * gen_shift_small_squares, -// cos(frameCount / 50) * gen_shift_small_squares -// ); -// rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); -// } - -// if (n > 0) -// rect( -// -spacing / (gen_offset_small_sq_i + n), -// -spacing / (gen_offset_small_sq_j + n), -// spacing / (n + 1), -// spacing / (n + 1) -// ); -// else rect(-spacing / 2, -spacing / 2, spacing, spacing); - -// pop(); -// } -// // strokeWeight(40); -// // point(i, j); -// } -// } -// } +function makeSquares(depth = gen_depth) { + colorMode(HSB); + let count_i = 0; + + for (let i = offset; i < w - offset; i += spacing) { + let count_j = 0; + count_i++; + + if (count_i > gen_num_total_squares) break; + + for (let j = offset; j < w - offset; j += spacing) { + count_j++; + + if (count_j > gen_num_total_squares) break; + + for (let n = 0; n < depth; n++) { + noFill(); + + if (n === 0) { + stroke(initHue, 100, 100); + fill( + initHue, + 100, + 100, + map( + sin( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 0.3 + ) + ); + } else { + stroke(compColor, map(n, 0, depth, 100, 0), 100); + fill( + compColor, + 100, + 100, + map( + cos( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 0.3 + ) + ); + } + + strokeWeight( + map( + sin( + gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed + ), + -1, + 1, + 0, + 1.5 + ) + ); + + push(); + translate(i + spacing / 2, j + spacing / 2); + + rotate( + i * gen_offset_i + + j * gen_offset_j + + frameCount / (gen_rotation_speed / (n + 1)) + ); + + if (n % 2 !== 0) { + translate( + sin(frameCount / 50) * gen_shift_small_squares, + cos(frameCount / 50) * gen_shift_small_squares + ); + rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); + } + + if (n > 0) + rect( + -spacing / (gen_offset_small_sq_i + n), + -spacing / (gen_offset_small_sq_j + n), + spacing / (n + 1), + spacing / (n + 1) + ); + else rect(-spacing / 2, -spacing / 2, spacing, spacing); + + pop(); + } + // strokeWeight(40); + // point(i, j); + } + } +} -// function addHandle() { -// fill(40); -// noStroke(); -// textAlign(RIGHT, BOTTOM); -// textFont(font); -// textSize(20); -// text('@jesi_rgb', w - 30, w - 30); -// } +function addHandle() { + fill(40); + noStroke(); + textAlign(RIGHT, BOTTOM); + textFont(font); + textSize(20); + text('@jesi_rgb', w - 30, w - 30); +} -// function mousePressed() { -// if (mouseButton === LEFT) { -// if (looping) { -// noLoop(); -// looping = false; -// } else { -// loop(); -// looping = true; -// } -// } -// } +function mousePressed() { + if (mouseButton === LEFT) { + if (looping) { + noLoop(); + looping = false; + } else { + loop(); + looping = true; + } + } +} -// function keyPressed() { -// console.log(key); -// switch (key) { -// // pressing the 's' key -// case 's': -// saveGif('mySketch', 1); -// break; +function keyPressed() { + console.log(key); + switch (key) { + // pressing the 's' key + case 's': + saveGif('mySketch', 1); + break; -// // pressing the '0' key -// case '0': -// frameCount = 0; -// loop(); -// noLoop(); -// break; + // pressing the '0' key + case '0': + frameCount = 0; + loop(); + noLoop(); + break; -// // pressing the ← key -// case 'ArrowLeft': -// frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); -// noLoop(); -// console.log(frameCount); -// break; + // pressing the ← key + case 'ArrowLeft': + frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); + noLoop(); + console.log(frameCount); + break; -// // pressing the → key -// case 'ArrowRights': -// frameCount += 1; -// noLoop(); -// console.log(frameCount); -// break; + // pressing the → key + case 'ArrowRights': + frameCount += 1; + noLoop(); + console.log(frameCount); + break; -// default: -// break; -// } -// } + default: + break; + } +} diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 0265c23741..4323c6e37a 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -327,9 +327,11 @@ p5.prototype.saveGif = async function(...args) { } } // we decide on one of this colors to be fully transparent - const transparentIndex = matchingPixelsInFrames[0]; + const transparentIndex = currFramePixels[matchingPixelsInFrames[0]]; // Apply palette to RGBA data to get an indexed bitmap - const indexedFrame = applyPalette(frames[i], globalPalette, { format }); + const indexedFrame = applyPalette(currFramePixels, globalPalette, { + format + }); for (let mp = 0; mp < matchingPixelsInFrames.length; mp++) { let samePixelIndex = matchingPixelsInFrames[mp]; From de52c273f3f3ae691d094c5e90538e59ded7fd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sat, 23 Jul 2022 10:55:21 +0200 Subject: [PATCH 085/177] push latest changes meeting --- lib/empty-example/sketch.js | 2 +- src/image/loading_displaying.js | 90 ++++++++++++++++----------------- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 4dbb7d70c3..844b6b4506 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -237,7 +237,7 @@ function keyPressed() { switch (key) { // pressing the 's' key case 's': - saveGif('mySketch', 1); + saveGif('mySketch', 6); break; // pressing the '0' key diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 4323c6e37a..bb17df7930 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -279,6 +279,8 @@ p5.prototype.saveGif = async function(...args) { frames.push(data); count++; + + await new Promise(resolve => setTimeout(resolve, 0)); } console.info('Frames processed, encoding gif. This may take a while...'); @@ -305,54 +307,50 @@ p5.prototype.saveGif = async function(...args) { // kinda digging a "hole" into the frame to see the pixels that where behind it // (which would be the exact same, so not noticeable changes) // this helps make the file smaller - if (i > 0) { - let currFramePixels = frames[i]; - let lastFramePixels = frames[i - 1]; - let matchingPixelsInFrames = []; - for (let p = 0; p < currFramePixels.length; p += 4) { - let currPixel = [ - currFramePixels[p], - currFramePixels[p + 1], - currFramePixels[p + 2], - currFramePixels[p + 3] - ]; - let lastPixel = [ - lastFramePixels[p], - lastFramePixels[p + 1], - lastFramePixels[p + 2], - lastFramePixels[p + 3] - ]; - if (pixelEquals(currPixel, lastPixel)) { - matchingPixelsInFrames.push(parseInt(p / 4)); - } - } - // we decide on one of this colors to be fully transparent - const transparentIndex = currFramePixels[matchingPixelsInFrames[0]]; - // Apply palette to RGBA data to get an indexed bitmap - const indexedFrame = applyPalette(currFramePixels, globalPalette, { - format - }); - - for (let mp = 0; mp < matchingPixelsInFrames.length; mp++) { - let samePixelIndex = matchingPixelsInFrames[mp]; - // here, we overwrite whatever color this pixel was assigned to - // with the color that we decided we are going to use as transparent. - // down in writeFrame we are going to tell the encoder that whenever - // it runs into "transparentIndex", just dig a hole there allowing to - // see through what was in the frame before it. - indexedFrame[samePixelIndex] = transparentIndex; + let currFramePixels = frames[i]; + let lastFramePixels = frames[i - 1]; + let matchingPixelsInFrames = []; + for (let p = 0; p < currFramePixels.length; p += 4) { + let currPixel = [ + currFramePixels[p], + currFramePixels[p + 1], + currFramePixels[p + 2], + currFramePixels[p + 3] + ]; + let lastPixel = [ + lastFramePixels[p], + lastFramePixels[p + 1], + lastFramePixels[p + 2], + lastFramePixels[p + 3] + ]; + if (pixelEquals(currPixel, lastPixel)) { + matchingPixelsInFrames.push(parseInt(p / 4)); } - // Write frame into the encoder - // if it's the first frame, also add what will be the global palette - - // all subsequent frames will just use the global palette - gif.writeFrame(indexedFrame, width_pd, height_pd, { - delay: 20, - transparent: true, - transparentIndex: transparentIndex, - dispose: 1 - }); } + // we decide on one of this colors to be fully transparent + // Apply palette to RGBA data to get an indexed bitmap + const indexedFrame = applyPalette(currFramePixels, globalPalette, { + format + }); + const transparentIndex = indexedFrame[matchingPixelsInFrames[0]]; + + for (let mp = 0; mp < matchingPixelsInFrames.length; mp++) { + let samePixelIndex = matchingPixelsInFrames[mp]; + // here, we overwrite whatever color this pixel was assigned to + // with the color that we decided we are going to use as transparent. + // down in writeFrame we are going to tell the encoder that whenever + // it runs into "transparentIndex", just dig a hole there allowing to + // see through what was in the frame before it. + indexedFrame[samePixelIndex] = transparentIndex; + } + // Write frame into the encoder + + gif.writeFrame(indexedFrame, width_pd, height_pd, { + delay: 20, + transparent: true, + transparentIndex: transparentIndex, + dispose: 1 + }); print('Frame: ' + i.toString()); // this just makes the process asynchronous, preventing From 904e91e5f48bc0f2f6dbae36fd2aeccead0e7f8f Mon Sep 17 00:00:00 2001 From: stampyzfanz <34364128+stampyzfanz@users.noreply.github.com> Date: Sat, 23 Jul 2022 20:57:10 +1000 Subject: [PATCH 086/177] Update checkbox labels in issue template. --- .../ISSUE_TEMPLATE/existing-feature-enhancement.yml | 10 +++++----- .github/ISSUE_TEMPLATE/feature-request.yml | 10 +++++----- .github/ISSUE_TEMPLATE/found-a-bug.yml | 10 +++++----- .github/labeler.yml | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml index 1b9a3f74b0..e36270b3a5 100644 --- a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml @@ -14,22 +14,22 @@ body: label: Most appropriate sub-area of p5.js? description: You may select more than one. options: - - label: Accessibility (Web Accessibility) + - label: Accessibility - label: Color - label: Core/Environment/Rendering - label: Data - label: DOM - label: Events - label: Image - - label: IO (Input/Output) + - label: IO - label: Math - label: Typography - label: Utilities - label: WebGL - - label: Build tools and processes + - label: Build Process - label: Unit Testing - - label: Friendly error system - - label: Localization + - label: Localization Tools + - label: Friendly Errors - label: Other (specify if possible) - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 46de3c6cdd..dbe6d7ba72 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -14,22 +14,22 @@ body: label: Most appropriate sub-area of p5.js? description: You may select more than one. options: - - label: Accessibility (Web Accessibility) + - label: Accessibility - label: Color - label: Core/Environment/Rendering - label: Data - label: DOM - label: Events - label: Image - - label: IO (Input/Output) + - label: IO - label: Math - label: Typography - label: Utilities - label: WebGL - - label: Build tools and processes + - label: Build Process - label: Unit Testing - - label: Friendly error system - - label: Localization + - label: Localization Tools + - label: Friendly Errors - label: Other (specify if possible) - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/found-a-bug.yml b/.github/ISSUE_TEMPLATE/found-a-bug.yml index d2f6899db9..f4a0b0520a 100644 --- a/.github/ISSUE_TEMPLATE/found-a-bug.yml +++ b/.github/ISSUE_TEMPLATE/found-a-bug.yml @@ -8,22 +8,22 @@ body: label: Most appropriate sub-area of p5.js? description: You may select more than one. options: - - label: Accessibility (Web Accessibility) + - label: Accessibility - label: Color - label: Core/Environment/Rendering - label: Data - label: DOM - label: Events - label: Image - - label: IO (Input/Output) + - label: IO - label: Math - label: Typography - label: Utilities - label: WebGL - - label: Build tools and processes + - label: Build Process - label: Unit Testing - - label: Friendly error system - - label: Localization + - label: Localization Tools + - label: Friendly Errors - label: Other (specify if possible) - type: input attributes: diff --git a/.github/labeler.yml b/.github/labeler.yml index 3274b9bd6d..0e87871f8b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,5 +1,5 @@ "Area:Accessibility": - - '\[X\] Accessibility \(Web Accessibility\)' + - '\[X\] Accessibility' "Area:Color": - '\[X\] Color' "Area:Core": @@ -13,7 +13,7 @@ "Area:Image": - '\[X\] Image' "Area:IO": - - '\[X\] IO \(Input/Output\)' + - '\[X\] IO' "Area:Math": - '\[X\] Math' "Area:Typography": @@ -23,10 +23,10 @@ "Area:WebGL": - '\[X\] WebGL' "Build Process": - - '\[X\] Build tools and processes' + - '\[X\] Build Process' "Unit Testing": - '\[X\] Unit Testing' "Localization": - - '\[X\] Localization' + - '\[X\] Localization Tools' "Friendly Errors": - - '\[X\] Friendly error system' + - '\[X\] Friendly Errors' From 647a7ca329e7e4da7e7544b70cf264f1510de002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 24 Jul 2022 09:47:32 +0200 Subject: [PATCH 087/177] add underscore to custom functions --- src/image/loading_displaying.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index bb17df7930..267ed71227 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -271,7 +271,7 @@ p5.prototype.saveGif = async function(...args) { busy sketches or low end devices might take longer to render some frames. So we just wait for the frame to be drawn and immediately save it to a buffer and continue - */ + */ redraw(); const data = this.drawingContext.getImageData(0, 0, width_pd, height_pd) @@ -289,7 +289,7 @@ p5.prototype.saveGif = async function(...args) { const format = 'rgb444'; // calculate the global palette for this set of frames - const globalPalette = generateGlobalPalette(frames, format); + const globalPalette = _generateGlobalPalette(frames, format); // we are going to iterate the frames in pairs, n-1 and n for (let i = 0; i < frames.length; i++) { @@ -323,7 +323,7 @@ p5.prototype.saveGif = async function(...args) { lastFramePixels[p + 2], lastFramePixels[p + 3] ]; - if (pixelEquals(currPixel, lastPixel)) { + if (_pixelEquals(currPixel, lastPixel)) { matchingPixelsInFrames.push(parseInt(p / 4)); } } @@ -372,7 +372,7 @@ p5.prototype.saveGif = async function(...args) { p5.prototype.downloadFile(blob, fileName, extension); }; -function generateGlobalPalette(frames, format) { +function _generateGlobalPalette(frames, format) { // for each frame, we'll keep track of the count of // every unique color. that is: how many times does // this particular color appear in every frame? @@ -410,7 +410,7 @@ function generateGlobalPalette(frames, format) { return colorsSortedByFreq.splice(0, 256); } -function pixelEquals(a, b) { +function _pixelEquals(a, b) { return ( Array.isArray(a) && Array.isArray(b) && From f222c9b91d20cddac99754b3e1bff19b9cab00c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 24 Jul 2022 10:53:56 +0200 Subject: [PATCH 088/177] solve transparency bug --- src/image/loading_displaying.js | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 267ed71227..ec2c6c82da 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -265,6 +265,19 @@ p5.prototype.saveGif = async function(...args) { // We first take every frame that we are going to use for the animation let frames = []; + + let p = undefined; + if (document.getElementById('progressBar') !== null) + document.getElementById('progressBar').remove(); + + p = createP(''); + p.id('progressBar'); + + p.style('font-size', '16px'); + p.style('font-family', 'Montserrat'); + p.style('background-color', '#ffffffa0'); + p.position(0, 0); + while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -280,6 +293,12 @@ p5.prototype.saveGif = async function(...args) { frames.push(data); count++; + p.html( + 'Saved frame ' + + frames.length.toString() + + ' out of ' + + nFrames.toString() + ); await new Promise(resolve => setTimeout(resolve, 0)); } console.info('Frames processed, encoding gif. This may take a while...'); @@ -332,16 +351,15 @@ p5.prototype.saveGif = async function(...args) { const indexedFrame = applyPalette(currFramePixels, globalPalette, { format }); - const transparentIndex = indexedFrame[matchingPixelsInFrames[0]]; + const transparentIndex = currFramePixels[matchingPixelsInFrames[0]]; - for (let mp = 0; mp < matchingPixelsInFrames.length; mp++) { - let samePixelIndex = matchingPixelsInFrames[mp]; + for (let mp of matchingPixelsInFrames) { // here, we overwrite whatever color this pixel was assigned to // with the color that we decided we are going to use as transparent. // down in writeFrame we are going to tell the encoder that whenever // it runs into "transparentIndex", just dig a hole there allowing to // see through what was in the frame before it. - indexedFrame[samePixelIndex] = transparentIndex; + indexedFrame[mp] = transparentIndex; } // Write frame into the encoder @@ -351,7 +369,10 @@ p5.prototype.saveGif = async function(...args) { transparentIndex: transparentIndex, dispose: 1 }); - print('Frame: ' + i.toString()); + + p.html( + 'Rendered frame ' + i.toString() + ' out of ' + nFrames.toString() + ); // this just makes the process asynchronous, preventing // that the encoding locks up the browser @@ -369,6 +390,7 @@ p5.prototype.saveGif = async function(...args) { type: 'image/gif' }); + p.html('Done. Downloading!🌸'); p5.prototype.downloadFile(blob, fileName, extension); }; From ead2b805ea0ebbf918956ffa26629b36eef1f417 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Mon, 25 Jul 2022 14:18:47 -0700 Subject: [PATCH 089/177] Add more stewards --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 84f5fec034..858e6c464f 100644 --- a/README.md +++ b/README.md @@ -66,17 +66,17 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | Area | Steward(s) | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Overall | [@qianqianye](https://github.com/qianqianye) | -| [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken) | +| [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | | [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese), [@aahdee](https://github.com/aahdee) | | [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth), [@davepagurek](https://github.com/davepagurek), [@jeffawang](https://github.com/jeffawang) | -| [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken) | +| [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | | [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye), [@SamirDhoke](https://github.com/SamirDhoke) | | [Events](https://github.com/processing/p5.js/tree/main/src/events) | [@limzykenneth](https://github.com/limzykenneth) | | [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko), [@KleoP](https://github.com/KleoP) | [IO](https://github.com/processing/p5.js/tree/main/src/io) | [@limzykenneth](https://github.com/limzykenneth) | | [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth), [@jeffawang](https://github.com/jeffawang), [@AdilRabbani](https://github.com/AdilRabbani) | | [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe), [@SarveshLimaye](https://github.com/SarveshLimaye) | -| [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken) | +| [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | | [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor); [@davepagurek](https://github.com/davepagurek); [@jeffawang](https://github.com/jeffawang); [@AdilRabbani](https://github.com/AdilRabbani) | | Build Process/Unit Testing | [@outofambit](https://github.com/outofambit), [@kungfuchicken](https://github.com/kungfuchicken) | | Localization Tools | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | From 592bb79379ba97bfecc830bdfa5abc82911b828a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Tue, 26 Jul 2022 10:55:45 +0200 Subject: [PATCH 090/177] added babel plugin before uglify for ES6 support --- Gruntfile.js | 18 ++++++++++++++++-- package-lock.json | 5 +++++ package.json | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2953b17d17..665913a193 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -259,6 +259,16 @@ module.exports = grunt => { } } }, + babel: { + options: { + presets: ['@babel/preset-env'] + }, + dist: { + files: { + 'lib/p5.pre-min.js': 'lib/p5.js' + } + } + }, // This minifies the javascript into a single file and adds a banner to the // front of the file. @@ -274,8 +284,8 @@ module.exports = grunt => { }, dist: { files: { - 'lib/p5.min.js': 'lib/p5.pre-min.js', - 'lib/modules/p5Custom.min.js': 'lib/modules/p5Custom.pre-min.js' + 'lib/p5.min.js': ['lib/p5.pre-min.js'], + 'lib/modules/p5Custom.min.js': ['lib/modules/p5Custom.pre-min.js'] } } }, @@ -511,10 +521,14 @@ module.exports = grunt => { grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-simple-nyc'); + //this library converts the ES6 JS to ES5 so it can be properly minified + grunt.loadNpmTasks('grunt-babel'); + // Create the multitasks. grunt.registerTask('build', [ 'browserify', 'browserify:min', + 'babel', 'uglify', 'browserify:test' ]); diff --git a/package-lock.json b/package-lock.json index 72d7ac4ea0..5f9af608ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6827,6 +6827,11 @@ } } }, + "grunt-babel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/grunt-babel/-/grunt-babel-8.0.0.tgz", + "integrity": "sha512-WuiZFvGzcyzlEoPIcY1snI234ydDWeWWV5bpnB7PZsOLHcDsxWKnrR1rMWEUsbdVPPjvIirwFNsuo4CbJmsdFQ==" + }, "grunt-cli": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", diff --git a/package.json b/package.json index f1d46de5ab..9dda281337 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "grunt-mocha-test": "^0.13.3", "grunt-newer": "^1.1.0", "grunt-simple-nyc": "^3.0.1", + "grunt-babel": "^8.0.0", "html-entities": "^1.3.1", "husky": "^4.2.3", "i18next": "^19.0.2", @@ -155,7 +156,6 @@ "not dead" ], "author": "", - "dependencies": {}, "husky": { "hooks": { "pre-commit": "lint-staged" From 18aabac1a923e6e297e82000bc66f5cab294632f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Tue, 26 Jul 2022 10:56:12 +0200 Subject: [PATCH 091/177] improved user feedback through p html elements --- lib/empty-example/index.html | 2 +- lib/empty-example/sketch.js | 2 +- src/image/loading_displaying.js | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/empty-example/index.html b/lib/empty-example/index.html index 788b9f3ee3..54b1bfdfe2 100644 --- a/lib/empty-example/index.html +++ b/lib/empty-example/index.html @@ -12,7 +12,7 @@ background-color: #1b1b1b; } - + diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 844b6b4506..7a53b24409 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -237,7 +237,7 @@ function keyPressed() { switch (key) { // pressing the 's' key case 's': - saveGif('mySketch', 6); + saveGif('mySketch', 2); break; // pressing the '0' key diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index ec2c6c82da..ef8688a573 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -252,10 +252,11 @@ p5.prototype.saveGif = async function(...args) { // we start on the frame set by the delay argument frameCount = nFramesDelay; - console.info( - 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' - ); + // console.info( + // 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' + // ); + const lastPixelDensity = this._pixelDensity; pixelDensity(1); const pd = this._pixelDensity; @@ -266,11 +267,10 @@ p5.prototype.saveGif = async function(...args) { // We first take every frame that we are going to use for the animation let frames = []; - let p = undefined; if (document.getElementById('progressBar') !== null) document.getElementById('progressBar').remove(); - p = createP(''); + let p = createP(''); p.id('progressBar'); p.style('font-size', '16px'); @@ -301,7 +301,11 @@ p5.prototype.saveGif = async function(...args) { ); await new Promise(resolve => setTimeout(resolve, 0)); } - console.info('Frames processed, encoding gif. This may take a while...'); + // console.info('Frames processed, encoding gif. This may take a while...'); + p.html('Frames processed, encoding gif. This may take a while...'); + + loop(); + pixelDensity(lastPixelDensity); // create the gif encoder and the colorspace format const gif = GIFEncoder(); @@ -381,8 +385,6 @@ p5.prototype.saveGif = async function(...args) { gif.finish(); - loop(); - // Get a direct typed array view into the buffer to avoid copying it const buffer = gif.bytesView(); const extension = 'gif'; @@ -403,7 +405,7 @@ function _generateGlobalPalette(frames, format) { // calculate the frequency table for the colors let colorFreq = {}; for (let f of frames) { - let currPalette = quantize(f, 1024, { format }); + let currPalette = quantize(f, 64, { format }); for (let color of currPalette) { // colors are in the format [r, g, b, (a)], as in [255, 127, 45, 255] // we'll convert the array to its string representation so it can be used as an index! From 7e795be72966b8e6c9113ca97da8ece6138b413b Mon Sep 17 00:00:00 2001 From: KevinGrajeda Date: Tue, 26 Jul 2022 20:32:27 -0500 Subject: [PATCH 092/177] setHeading() using current anglemode --- src/math/p5.Vector.js | 1 + test/unit/math/p5.Vector.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index d7ba19b7e5..9ca2da949a 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1490,6 +1490,7 @@ p5.Vector.prototype.heading = function heading() { */ p5.Vector.prototype.setHeading = function setHeading(a) { + if (this.isPInst) a = this._toRadians(a); let m = this.mag(); this.x = m * Math.cos(a); this.y = m * Math.sin(a); diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index 7c317c4822..f09e141bd1 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -18,15 +18,28 @@ suite('p5.Vector', function() { }); var v; - suite('setHeading', function() { + suite('p5.prototype.setHeading() RADIANS', function() { setup(function() { + myp5.angleMode(RADIANS); v = myp5.createVector(1, 1); v.setHeading(1); }); - test('should have heading() value of 1', function() { + test('should have heading() value of 1 (RADIANS)', function() { assert.closeTo(v.heading(), 1, 0.001); }); }); + + suite('p5.prototype.setHeading() DEGREES', function() { + setup(function() { + myp5.angleMode(DEGREES); + v = myp5.createVector(1, 1); + v.setHeading(1); + }); + test('should have heading() value of 1 (DEGREES)', function() { + assert.closeTo(v.heading(), 1, 0.001); + }); + }); + suite('p5.prototype.createVector()', function() { setup(function() { v = myp5.createVector(); From c06ad37f980291ea39b131d8cee0765124dee287 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Wed, 27 Jul 2022 18:26:25 -0700 Subject: [PATCH 093/177] Update issue label contributor doc with new labels --- contributor_docs/issue_labels.md | 99 ++++++++++++-------------------- 1 file changed, 37 insertions(+), 62 deletions(-) diff --git a/contributor_docs/issue_labels.md b/contributor_docs/issue_labels.md index 3de674a668..bd45e54603 100644 --- a/contributor_docs/issue_labels.md +++ b/contributor_docs/issue_labels.md @@ -2,71 +2,46 @@ p5.js uses a set of labels to help sort and organize issues. -All issues should have labels applied to indicate severity, level of difficulty, and which components/areas are affected. Additional status tags may be applied to indicate a resolution or type of bug (for example, duplicate issues). +All issues should have at least two labels to indicate the status and which areas are affected. ## Status -Label | Usage -------------------- | ------------- -help_wanted | Not sure how to fix, looking for contribution, easy access point for people (indicates issue can be claimed by new developer -inconsistent_style | Unclean code, confusing syntax, maybe insufficient documentation -duplicate | Issue already noted elsewhere -missing_test | Feature or API in need of automated test -wont_fix | Legitimate issue, but won't be addressed due to community agreed upon scope -gsoc | problem already being addressed by Google Summer of Code -invalid | No longer relevant (for example, feature request in old API), not actually a problem -discussion | Know what the problem is, need community input to determine solution -question | Not sure what problem is/if there is a problem, requires clarification -feature | An addition or improvement to the codebase -regression | Function/feature once worked, but has since broken. Useful for identifying unstable features or components - - -## Severity -Classify the bug's impact on p5.js users and developers. - -Label | Usage -------------------- | ------------- -severity:critical | Blocks work of other developers (for example a broken build); or causes data loss for a library or IDE user -severity:major | Loss of functionality in an important component -severity:minor | A small item not likely to be seen by many users; or something that will only be a minor annoyance if encountered by a user; or something with a known work-around - -## Difficulty Level -Tag bugs and feature requests according to difficulty level. Help identify bugs that can be tackled by beginners or new contributors, or items that will take substantial effort even from experienced contributors. - -Label | Usage -------------------- | ------------- -level:bite size | Easily squashed, could be tackled by a new/junior developer -level:moderate | Requires a sizable amount of work or familiarity with code base -level:advanced | Requires a large amount of work and possibly an invasive fix or re-architecture -level:unknown | Difficulty not known by person filing the issue - -## Area +| Label | Usage | +| ----------------- | -------------------------------------------------------------------- | +| Announcement | Announcement from p5.js leads/stewards | +| Bug | | +| Dependencies | | +| Discussion | Know what the problem is, need community input to determine solution | +| Enhancement | An improvement to the codebase | +| Feature Request | An addition to the codebase | +| Help Wanted | Not sure how to fix, looking for help from contributors | +| Known Issue | | +| Good First Issue | Issues recommended for first time contributors | +| More Info Needed | Require more info to illustrate the issue | +| Please Help Label | Not sure which label should be added | + + +## Areas Indicate the part of the code base affected by the issue. -* area:3d -* area:color -* area:core -* area:documentation -* area:dom -* area:events -* area:examples -* area:image -* area:io -* area:math -* area:tutorial -* area:typography - -## OS/Browser -When an issue affects ONLY a specific operating system and/or browser, tag the issue appropriately. +All the Area labels are now coordinated with the [src folder](https://github.com/processing/p5.js/tree/main/src) structure. +* Area:Accessibility +* Area:Color +* Area:Core +* Area:Data +* Area:DOM +* Area:Events +* Area:Image +* Area:IO +* Area:Math +* Area:Typography +* Area:Utilities +* Area:WebGL + +All the labels below are coordinated with the rest of the steward [focus areas](https://github.com/processing/p5.js#stewards). +* Build Process +* Unit Testing +* Localization Tools +* Friendly Errors +* Documentation -* chrome -* ie -* safari -* opera -* firefox -* android -* ios -* windows_mobile -* windows -* osx -* linux \ No newline at end of file From 17e0394d3dbc982bf0ee7f17c5c314e74f276926 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 28 Jul 2022 14:18:50 -0700 Subject: [PATCH 094/177] Update contributor docs README --- contributor_docs/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contributor_docs/README.md b/contributor_docs/README.md index 56f4de4984..bba1f640ef 100644 --- a/contributor_docs/README.md +++ b/contributor_docs/README.md @@ -4,7 +4,7 @@ Thanks for your interest in contributing to p5.js! Our community values contributions of all forms and seeks to expand the meaning of the word "contributor" as far and wide as possible. It includes documentation, teaching, writing code, making art, writing, design, activism, organizing, curating, or anything else you might imagine. [Our community page](https://p5js.org/community/#contribute) gives an overview of some different ways to get involved and contribute. For technical contributions, read on to get started. -This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Add yourself to the [readme](https://github.com/processing/p5.js/blob/main/README.md#contributors) by following the [instructions here](https://github.com/processing/p5.js/issues/2309)! Or comment in the [GitHub issues](https://github.com/processing/p5.js/issues) with your contribution and we'll add you. +This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Add yourself to the [readme](https://github.com/processing/p5.js/blob/main/README.md#contributors) by following the [instructions here](https://github.com/processing/p5.js/issues/2309)! Or comment in the [GitHub issues](https://github.com/processing/p5.js/issues) with your contribution and we'll add you. The contributor docs are published on p5.js [website](https://p5js.org/contributor-docs/#/), and hosted on p5.js [GitHub repository](https://github.com/processing/p5.js/tree/main/contributor_docs). # Prioritizing access @@ -14,8 +14,8 @@ We are prioritizing work that expands access (inclusion and accessibility) to p5 The overarching p5.js project includes some repositories other than this one: -- **[p5.js](https://github.com/processing/p5.js)**: This repository contains the source code for the p5.js library. The [user-facing p5.js reference manual](https://p5js.org/reference/) is also generated from the [JSDoc](http://usejsdoc.org/) comments included in this source code. It is maintained by [Qianqian Q Ye](https://github.com/qianqianye) and [evelyn masso](https://github.com/outofambit). -- **[p5.js-website](https://github.com/processing/p5.js-website)**: This repository contains most of the code for the [p5.js website](http://p5js.org), with the exception of the reference manual. It is maintained by [Kenneth Lim](https://github.com/limzykenneth), [Qianqian Q Ye](https://github.com/qianqianye) and [evelyn masso](https://github.com/outofambit). +- **[p5.js](https://github.com/processing/p5.js)**: This repository contains the source code for the p5.js library. The [user-facing p5.js reference manual](https://p5js.org/reference/) is also generated from the [JSDoc](https://jsdoc.app/) comments included in this source code. It is maintained by [Qianqian Ye](https://github.com/qianqianye) and a group of [stewards](https://github.com/processing/p5.js#stewards). +- **[p5.js-website](https://github.com/processing/p5.js-website)**: This repository contains most of the code for the [p5.js website](http://p5js.org), with the exception of the reference manual. It is maintained by [Qianqian Ye](https://github.com/qianqianye), [Kenneth Lim](https://github.com/limzykenneth), and and a group of [stewards](https://github.com/processing/p5.js-website#stewards). - **[p5.js-sound](https://github.com/processing/p5.js-sound)**: This repository contains the p5.sound.js library. It is maintained by [Jason Sigal](https://github.com/therewasaguy). - **[p5.js-web-editor](https://github.com/processing/p5.js-web-editor)**: This repository contains the source code for the [p5.js web editor](https://editor.p5js.org). It is maintained by [Cassie Tarakajian](https://github.com/catarak). Note that the older [p5.js editor](https://github.com/processing/p5.js-editor) is now deprecated. From 2ef0ceff774655839dcaa0df0a117840af339093 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 28 Jul 2022 14:29:35 -0700 Subject: [PATCH 095/177] fix typo on contributor doc README --- contributor_docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/README.md b/contributor_docs/README.md index bba1f640ef..de7f4ababe 100644 --- a/contributor_docs/README.md +++ b/contributor_docs/README.md @@ -15,7 +15,7 @@ We are prioritizing work that expands access (inclusion and accessibility) to p5 The overarching p5.js project includes some repositories other than this one: - **[p5.js](https://github.com/processing/p5.js)**: This repository contains the source code for the p5.js library. The [user-facing p5.js reference manual](https://p5js.org/reference/) is also generated from the [JSDoc](https://jsdoc.app/) comments included in this source code. It is maintained by [Qianqian Ye](https://github.com/qianqianye) and a group of [stewards](https://github.com/processing/p5.js#stewards). -- **[p5.js-website](https://github.com/processing/p5.js-website)**: This repository contains most of the code for the [p5.js website](http://p5js.org), with the exception of the reference manual. It is maintained by [Qianqian Ye](https://github.com/qianqianye), [Kenneth Lim](https://github.com/limzykenneth), and and a group of [stewards](https://github.com/processing/p5.js-website#stewards). +- **[p5.js-website](https://github.com/processing/p5.js-website)**: This repository contains most of the code for the [p5.js website](http://p5js.org), with the exception of the reference manual. It is maintained by [Qianqian Ye](https://github.com/qianqianye), [Kenneth Lim](https://github.com/limzykenneth), and a group of [stewards](https://github.com/processing/p5.js-website#stewards). - **[p5.js-sound](https://github.com/processing/p5.js-sound)**: This repository contains the p5.sound.js library. It is maintained by [Jason Sigal](https://github.com/therewasaguy). - **[p5.js-web-editor](https://github.com/processing/p5.js-web-editor)**: This repository contains the source code for the [p5.js web editor](https://editor.p5js.org). It is maintained by [Cassie Tarakajian](https://github.com/catarak). Note that the older [p5.js editor](https://github.com/processing/p5.js-editor) is now deprecated. From 10bb489b1a98d67811dfc9a4d899c2c31676dc2f Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 29 Jul 2022 08:43:33 +0800 Subject: [PATCH 096/177] Fix applyMatrix example missing from reference page --- src/core/transform.js | 60 +++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/core/transform.js b/src/core/transform.js index f77523ab64..b14d14e7a9 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -30,36 +30,6 @@ import p5 from './main'; * @method applyMatrix * @param {Array} arr an array of numbers - should be 6 or 16 length (2*3 or 4*4 matrix values) * @chainable - */ -/** - * @method applyMatrix - * @param {Number} a numbers which define the 2×3 or 4x4 matrix to be multiplied - * @param {Number} b numbers which define the 2×3 or 4x4 matrix to be multiplied - * @param {Number} c numbers which define the 2×3 or 4x4 matrix to be multiplied - * @param {Number} d numbers which define the 2×3 or 4x4 matrix to be multiplied - * @param {Number} e numbers which define the 2×3 or 4x4 matrix to be multiplied - * @param {Number} f numbers which define the 2×3 or 4x4 matrix to be multiplied - * @chainable - */ -/** - * @method applyMatrix - * @param {Number} a - * @param {Number} b - * @param {Number} c - * @param {Number} d - * @param {Number} e - * @param {Number} f - * @param {Number} g numbers which define the 4x4 matrix to be multiplied - * @param {Number} h numbers which define the 4x4 matrix to be multiplied - * @param {Number} i numbers which define the 4x4 matrix to be multiplied - * @param {Number} j numbers which define the 4x4 matrix to be multiplied - * @param {Number} k numbers which define the 4x4 matrix to be multiplied - * @param {Number} l numbers which define the 4x4 matrix to be multiplied - * @param {Number} m numbers which define the 4x4 matrix to be multiplied - * @param {Number} n numbers which define the 4x4 matrix to be multiplied - * @param {Number} o numbers which define the 4x4 matrix to be multiplied - * @param {Number} p numbers which define the 4x4 matrix to be multiplied - * @chainable * @example *
* @@ -183,6 +153,36 @@ import p5 from './main'; * A rectangle shearing * A rectangle in the upper left corner */ +/** + * @method applyMatrix + * @param {Number} a numbers which define the 2×3 or 4x4 matrix to be multiplied + * @param {Number} b numbers which define the 2×3 or 4x4 matrix to be multiplied + * @param {Number} c numbers which define the 2×3 or 4x4 matrix to be multiplied + * @param {Number} d numbers which define the 2×3 or 4x4 matrix to be multiplied + * @param {Number} e numbers which define the 2×3 or 4x4 matrix to be multiplied + * @param {Number} f numbers which define the 2×3 or 4x4 matrix to be multiplied + * @chainable + */ +/** + * @method applyMatrix + * @param {Number} a + * @param {Number} b + * @param {Number} c + * @param {Number} d + * @param {Number} e + * @param {Number} f + * @param {Number} g numbers which define the 4x4 matrix to be multiplied + * @param {Number} h numbers which define the 4x4 matrix to be multiplied + * @param {Number} i numbers which define the 4x4 matrix to be multiplied + * @param {Number} j numbers which define the 4x4 matrix to be multiplied + * @param {Number} k numbers which define the 4x4 matrix to be multiplied + * @param {Number} l numbers which define the 4x4 matrix to be multiplied + * @param {Number} m numbers which define the 4x4 matrix to be multiplied + * @param {Number} n numbers which define the 4x4 matrix to be multiplied + * @param {Number} o numbers which define the 4x4 matrix to be multiplied + * @param {Number} p numbers which define the 4x4 matrix to be multiplied + * @chainable + */ p5.prototype.applyMatrix = function() { let isTypedArray = arguments[0] instanceof Object.getPrototypeOf(Uint8Array); if (Array.isArray(arguments[0]) || isTypedArray) { From 603b3c95c5c7cad445e84751117aa0687b575999 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Fri, 29 Jul 2022 15:25:59 -0700 Subject: [PATCH 097/177] update label name --- .github/ISSUE_TEMPLATE/discussion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/discussion.yml b/.github/ISSUE_TEMPLATE/discussion.yml index 5946f12e9d..115fb834cb 100644 --- a/.github/ISSUE_TEMPLATE/discussion.yml +++ b/.github/ISSUE_TEMPLATE/discussion.yml @@ -1,6 +1,6 @@ name: 💭 Discussion description: This template is for starting a discussion. -labels: [discussion] +labels: [Discussion] body: - type: textarea attributes: From ea0daeb56d4d735200f3eea0e2f12727a9d277c3 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Fri, 29 Jul 2022 15:36:11 -0700 Subject: [PATCH 098/177] Update Issue Template label --- .github/ISSUE_TEMPLATE/existing-feature-enhancement.yml | 2 +- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- .github/ISSUE_TEMPLATE/found-a-bug.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml index e36270b3a5..b549520e81 100644 --- a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml @@ -1,6 +1,6 @@ name: 💡 Existing Feature Enhancement description: This template is for suggesting an improvement for an existing feature. -labels: [enhancement] +labels: [Enhancement] body: - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index dbe6d7ba72..dbe464acec 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,6 +1,6 @@ name: 🌱 New Feature Request description: This template is for requesting a new feature be added. -labels: [feature request] +labels: [Feature Request] body: - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/found-a-bug.yml b/.github/ISSUE_TEMPLATE/found-a-bug.yml index f4a0b0520a..e083a135a0 100644 --- a/.github/ISSUE_TEMPLATE/found-a-bug.yml +++ b/.github/ISSUE_TEMPLATE/found-a-bug.yml @@ -1,6 +1,6 @@ name: 🐛 Found a Bug description: This template is for reporting bugs (broken or incorrect behaviour). If you have questions about your own code, please visit our forum discourse.processing.org instead. -labels: [bug] +labels: [Bug] body: - type: checkboxes id: sub-area From 9618ac814c826dc30604ecf1caeb5c6c1c9e5606 Mon Sep 17 00:00:00 2001 From: stampyzfanz <34364128+stampyzfanz@users.noreply.github.com> Date: Sat, 30 Jul 2022 10:20:10 +1000 Subject: [PATCH 099/177] Fix regex in issue labeler to allow for more whitespace and capitalisation --- .github/labeler.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 0e87871f8b..cf8b8c0109 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,32 +1,32 @@ "Area:Accessibility": - - '\[X\] Accessibility' + - '\[[xX]\]\s*Accessibility' "Area:Color": - - '\[X\] Color' + - '\[[xX]\]\s*Color' "Area:Core": - - '\[X\] Core' + - '\[[xX]\]\s*Core' "Area:Data": - - '\[X\] Data' + - '\[[xX]\]\s*Data' "Area:DOM": - - '\[X\] DOM' + - '\[[xX]\]\s*DOM' "Area:Events": - - '\[X\] Events' + - '\[[xX]\]\s*Events' "Area:Image": - - '\[X\] Image' + - '\[[xX]\]\s*Image' "Area:IO": - - '\[X\] IO' + - '\[[xX]\]\s*IO' "Area:Math": - - '\[X\] Math' + - '\[[xX]\]\s*Math' "Area:Typography": - - '\[X\] Typography' + - '\[[xX]\]\s*Typography' "Area:Utilities": - - '\[X\] Utilities' + - '\[[xX]\]\s*Utilities' "Area:WebGL": - - '\[X\] WebGL' + - '\[[xX]\]\s*WebGL' "Build Process": - - '\[X\] Build Process' + - '\[[xX]\]\s*Build Process' "Unit Testing": - - '\[X\] Unit Testing' + - '\[[xX]\]\s*Unit Testing' "Localization": - - '\[X\] Localization Tools' + - '\[[xX]\]\s*Localization Tools' "Friendly Errors": - - '\[X\] Friendly Errors' + - '\[[xX]\]\s*Friendly Errors' From ce18240a594f2aa0bef73ac4da51ea5d90f7fd30 Mon Sep 17 00:00:00 2001 From: stampyzfanz <34364128+stampyzfanz@users.noreply.github.com> Date: Sat, 30 Jul 2022 11:20:18 +1000 Subject: [PATCH 100/177] Grant write permission to issues --- .github/workflows/labeler.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index f5ae4b407b..48d99cfd86 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -2,7 +2,8 @@ name: "Issue Labeler" on: issues: types: [opened, edited] - +permissions: + issues: write jobs: triage: runs-on: ubuntu-latest From fe1c59dfaf12bab96eb968615544c6372e5d31d0 Mon Sep 17 00:00:00 2001 From: Yifan Mai Date: Sat, 30 Jul 2022 16:54:43 -0700 Subject: [PATCH 101/177] Test mask on current gif frame --- src/image/p5.Image.js | 13 +++++-------- test/unit/image/p5.Image.js | 11 +++++++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 53b77750f6..6bbb9fdf11 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -654,14 +654,7 @@ p5.Image.prototype.mask = function(p5Image) { this.drawingContext.globalCompositeOperation = 'destination-in'; if (this.gifProperties) { - const prevFrameData = this.drawingContext.getImageData( - 0, - 0, - this.width, - this.height - ); for (let i = 0; i < this.gifProperties.frames.length; i++) { - this.drawingContext.clearRect(0, 0, this.width, this.height); this.drawingContext.putImageData( this.gifProperties.frames[i].image, 0, @@ -675,7 +668,11 @@ p5.Image.prototype.mask = function(p5Image) { this.height ); } - this.drawingContext.putImageData(prevFrameData, 0, 0); + this.drawingContext.putImageData( + this.gifProperties.frames[this.gifProperties.displayIndex].image, + 0, + 0 + ); } else { p5.Image.prototype.copy.apply(this, copyArgs); } diff --git a/test/unit/image/p5.Image.js b/test/unit/image/p5.Image.js index 8b30802d15..2600be63ff 100644 --- a/test/unit/image/p5.Image.js +++ b/test/unit/image/p5.Image.js @@ -91,13 +91,20 @@ suite('p5.Image', function() { mask.loadPixels(); for (let i = 0; i < mask.width; i++) { for (let j = 0; j < mask.height; j++) { - const alpha = j < img.height < 2 ? 255 : 0; + const alpha = j < img.height / 2 ? 255 : 0; mask.set(i, j, myp5.color(0, 0, 0, alpha)); } } mask.updatePixels(); img.mask(mask); + img.loadPixels(); + for (let i = 0; i < img.width; i++) { + for (let j = 0; j < img.height; j++) { + const alpha = j < img.height / 2 ? 255 : 0; + assert.strictEqual(img.get(i, j)[3], alpha); + } + } for ( frameIndex = 0; frameIndex < img.gifProperties.numFrames; @@ -107,7 +114,7 @@ suite('p5.Image', function() { for (let i = 0; i < img.width; i++) { for (let j = 0; j < img.height; j++) { const index = 4 * (i + j * img.width) + 3; - const alpha = j < img.height < 2 ? 255 : 0; + const alpha = j < img.height / 2 ? 255 : 0; assert.strictEqual(frameData[index], alpha); } } From 94b83c7cbbe9c963bae042cca927375b61d22003 Mon Sep 17 00:00:00 2001 From: msed21 Date: Sun, 31 Jul 2022 02:15:22 -0500 Subject: [PATCH 102/177] Fix issue #5702 --- src/dom/dom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dom/dom.js b/src/dom/dom.js index eca5fb248a..c15484496a 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -1993,8 +1993,8 @@ p5.Element.prototype.style = function(prop, val) { ) { let styles = window.getComputedStyle(self.elt); let styleVal = styles.getPropertyValue(prop); - let numVal = styleVal.replace(/\D+/g, ''); - this[prop] = parseInt(numVal, 10); + let numVal = styleVal.replace(/[^\d.]/g, ''); + this[prop] = Math.round(parseFloat(numVal, 10)); } } return this; From c267dab0b594b9b3b77de0123b3cbbff50de1159 Mon Sep 17 00:00:00 2001 From: Ikebot <56776763+IkeB108@users.noreply.github.com> Date: Mon, 1 Aug 2022 15:10:35 -0700 Subject: [PATCH 103/177] Update .all-contributorsrc --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index c8f6e2869a..a3bcc32327 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3167,6 +3167,15 @@ "contributions": [ "translation" ] + }, + { + "login": "IkeB108", + "name": "Ike Bischof", + "avatar_url": "https://avatars.githubusercontent.com/u/56776763?v=4", + "profile": "https://ikebot108.weebly.com/", + "contributions": [ + "code" + ] } ], "repoType": "github", From bbb509d912ffca2ad250d4cc0e524cbc934748e9 Mon Sep 17 00:00:00 2001 From: Caleb Foss Date: Tue, 2 Aug 2022 15:30:18 -0500 Subject: [PATCH 104/177] Correct typo on box() in inline documentation "Height" corrected to "height" --- src/webgl/3d_primitives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index ef6a973cb0..251ee18adb 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -99,7 +99,7 @@ p5.prototype.plane = function(width, height, detailX, detailY) { * Draw a box with given width, height and depth * @method box * @param {Number} [width] width of the box - * @param {Number} [Height] height of the box + * @param {Number} [height] height of the box * @param {Number} [depth] depth of the box * @param {Integer} [detailX] Optional number of triangle * subdivisions in x-dimension From 3d76eb163107528c4d15fa33c811c7a3227ac740 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:24:46 +0000 Subject: [PATCH 105/177] docs: update README.md [skip ci] --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 858e6c464f..1fc48e495a 100644 --- a/README.md +++ b/README.md @@ -577,6 +577,10 @@ We recognize all types of contributions. This project follows the [all-contribut
anniezhengg

💻 🎨
Seung-Gi Kim(David)

🌍 + +
Ike Bischof

💻 +
Ong Zhi Zheng

🔌 + From 7aff698baf0b5f7ecd080da2ce804d69c0bbacec Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:24:47 +0000 Subject: [PATCH 106/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index a3bcc32327..afea2275e7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3176,6 +3176,15 @@ "contributions": [ "code" ] + }, + { + "login": "ongzzzzzz", + "name": "Ong Zhi Zheng", + "avatar_url": "https://avatars.githubusercontent.com/u/47311100?v=4", + "profile": "https://ongzz.ml", + "contributions": [ + "plugin" + ] } ], "repoType": "github", From 69f19edb2c65473d436b17244de3d9009ba60908 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:30:15 +0000 Subject: [PATCH 107/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1fc48e495a..be52227f7c 100644 --- a/README.md +++ b/README.md @@ -580,6 +580,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Ike Bischof

💻
Ong Zhi Zheng

🔌 +
bsubbaraman

🔌 From ace7b02077e4d8e976c16bbbf4e7282cc6f3ee63 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:30:16 +0000 Subject: [PATCH 108/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index afea2275e7..4d0ab07987 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3185,6 +3185,15 @@ "contributions": [ "plugin" ] + }, + { + "login": "bsubbaraman", + "name": "bsubbaraman", + "avatar_url": "https://avatars.githubusercontent.com/u/11969085?v=4", + "profile": "https://github.com/bsubbaraman", + "contributions": [ + "plugin" + ] } ], "repoType": "github", From 0c5459fb81410a252bdff84fff9cfd53c0d5d2d3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 3 Aug 2022 22:05:50 +0000 Subject: [PATCH 109/177] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index be52227f7c..e109c689d7 100644 --- a/README.md +++ b/README.md @@ -581,6 +581,7 @@ We recognize all types of contributions. This project follows the [all-contribut
Ike Bischof

💻
Ong Zhi Zheng

🔌
bsubbaraman

🔌 +
Jenna deBoisblanc

🔌 From 0b6ff3792a42cc89900c5e3194abbc517dafe828 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 3 Aug 2022 22:05:51 +0000 Subject: [PATCH 110/177] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4d0ab07987..9ce7dbd9a1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3194,6 +3194,15 @@ "contributions": [ "plugin" ] + }, + { + "login": "jdeboi", + "name": "Jenna deBoisblanc", + "avatar_url": "https://avatars.githubusercontent.com/u/1548679?v=4", + "profile": "http://jdeboi.com", + "contributions": [ + "plugin" + ] } ], "repoType": "github", From b0e07dc7d910f7694eded7dac263323fb464beef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 4 Aug 2022 10:39:45 +0200 Subject: [PATCH 111/177] add back old saveGif function as encodeAndDownloadGif --- src/image/image.js | 227 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/src/image/image.js b/src/image/image.js index 50cc364da5..2f69b1c969 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -184,6 +184,233 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; +// this is the old saveGif, left here for compatibility purposes +p5.prototype.encodeAndDownloadGif = function(pImg, filename) { + const props = pImg.gifProperties; + + //convert loopLimit back into Netscape Block formatting + let loopLimit = props.loopLimit; + if (loopLimit === 1) { + loopLimit = null; + } else if (loopLimit === null) { + loopLimit = 0; + } + const buffer = new Uint8Array(pImg.width * pImg.height * props.numFrames); + + const allFramesPixelColors = []; + + // Used to determine the occurrence of unique palettes and the frames + // which use them + const paletteFreqsAndFrames = {}; + + // Pass 1: + //loop over frames and get the frequency of each palette + for (let i = 0; i < props.numFrames; i++) { + const paletteSet = new Set(); + const data = props.frames[i].image.data; + const dataLength = data.length; + // The color for each pixel in this frame ( for easier lookup later ) + const pixelColors = new Uint32Array(pImg.width * pImg.height); + for (let j = 0, k = 0; j < dataLength; j += 4, k++) { + const r = data[j + 0]; + const g = data[j + 1]; + const b = data[j + 2]; + const color = (r << 16) | (g << 8) | (b << 0); + paletteSet.add(color); + + // What color does this pixel have in this frame ? + pixelColors[k] = color; + } + + // A way to put use the entire palette as an object key + const paletteStr = [...paletteSet].sort().toString(); + if (paletteFreqsAndFrames[paletteStr] === undefined) { + paletteFreqsAndFrames[paletteStr] = { freq: 1, frames: [i] }; + } else { + paletteFreqsAndFrames[paletteStr].freq += 1; + paletteFreqsAndFrames[paletteStr].frames.push(i); + } + + allFramesPixelColors.push(pixelColors); + } + + let framesUsingGlobalPalette = []; + + // Now to build the global palette + // Sort all the unique palettes in descending order of their occurrence + const palettesSortedByFreq = Object.keys(paletteFreqsAndFrames).sort(function( + a, + b + ) { + return paletteFreqsAndFrames[b].freq - paletteFreqsAndFrames[a].freq; + }); + + // The initial global palette is the one with the most occurrence + const globalPalette = palettesSortedByFreq[0] + .split(',') + .map(a => parseInt(a)); + + framesUsingGlobalPalette = framesUsingGlobalPalette.concat( + paletteFreqsAndFrames[globalPalette].frames + ); + + const globalPaletteSet = new Set(globalPalette); + + // Build a more complete global palette + // Iterate over the remaining palettes in the order of + // their occurrence and see if the colors in this palette which are + // not in the global palette can be added there, while keeping the length + // of the global palette <= 256 + for (let i = 1; i < palettesSortedByFreq.length; i++) { + const palette = palettesSortedByFreq[i].split(',').map(a => parseInt(a)); + + const difference = palette.filter(x => !globalPaletteSet.has(x)); + if (globalPalette.length + difference.length <= 256) { + for (let j = 0; j < difference.length; j++) { + globalPalette.push(difference[j]); + globalPaletteSet.add(difference[j]); + } + + // All frames using this palette now use the global palette + framesUsingGlobalPalette = framesUsingGlobalPalette.concat( + paletteFreqsAndFrames[palettesSortedByFreq[i]].frames + ); + } + } + + framesUsingGlobalPalette = new Set(framesUsingGlobalPalette); + + // Build a lookup table of the index of each color in the global palette + // Maps a color to its index + const globalIndicesLookup = {}; + for (let i = 0; i < globalPalette.length; i++) { + if (!globalIndicesLookup[globalPalette[i]]) { + globalIndicesLookup[globalPalette[i]] = i; + } + } + + // force palette to be power of 2 + let powof2 = 1; + while (powof2 < globalPalette.length) { + powof2 <<= 1; + } + globalPalette.length = powof2; + + // global opts + const opts = { + loop: loopLimit, + palette: new Uint32Array(globalPalette) + }; + const gifWriter = new omggif.GifWriter(buffer, pImg.width, pImg.height, opts); + let previousFrame = {}; + + // Pass 2 + // Determine if the frame needs a local palette + // Also apply transparency optimization. This function will often blow up + // the size of a GIF if not for transparency. If a pixel in one frame has + // the same color in the previous frame, that pixel can be marked as + // transparent. We decide one particular color as transparent and make all + // transparent pixels take this color. This helps in later in compression. + for (let i = 0; i < props.numFrames; i++) { + const localPaletteRequired = !framesUsingGlobalPalette.has(i); + const palette = localPaletteRequired ? [] : globalPalette; + const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height); + + // Lookup table mapping color to its indices + const colorIndicesLookup = {}; + + // All the colors that cannot be marked transparent in this frame + const cannotBeTransparent = new Set(); + + for (let k = 0; k < allFramesPixelColors[i].length; k++) { + const color = allFramesPixelColors[i][k]; + if (localPaletteRequired) { + if (colorIndicesLookup[color] === undefined) { + colorIndicesLookup[color] = palette.length; + palette.push(color); + } + pixelPaletteIndex[k] = colorIndicesLookup[color]; + } else { + pixelPaletteIndex[k] = globalIndicesLookup[color]; + } + + if (i > 0) { + // If even one pixel of this color has changed in this frame + // from the previous frame, we cannot mark it as transparent + if (allFramesPixelColors[i - 1][k] !== color) { + cannotBeTransparent.add(color); + } + } + } + + const frameOpts = {}; + + // Transparency optimization + const canBeTransparent = palette.filter(a => !cannotBeTransparent.has(a)); + if (canBeTransparent.length > 0) { + // Select a color to mark as transparent + const transparent = canBeTransparent[0]; + const transparentIndex = localPaletteRequired + ? colorIndicesLookup[transparent] + : globalIndicesLookup[transparent]; + if (i > 0) { + for (let k = 0; k < allFramesPixelColors[i].length; k++) { + // If this pixel in this frame has the same color in previous frame + if (allFramesPixelColors[i - 1][k] === allFramesPixelColors[i][k]) { + pixelPaletteIndex[k] = transparentIndex; + } + } + frameOpts.transparent = transparentIndex; + // If this frame has any transparency, do not dispose the previous frame + previousFrame.frameOpts.disposal = 1; + } + } + frameOpts.delay = props.frames[i].delay / 10; // Move timing back into GIF formatting + if (localPaletteRequired) { + // force palette to be power of 2 + let powof2 = 1; + while (powof2 < palette.length) { + powof2 <<= 1; + } + palette.length = powof2; + frameOpts.palette = new Uint32Array(palette); + } + if (i > 0) { + // add the frame that came before the current one + gifWriter.addFrame( + 0, + 0, + pImg.width, + pImg.height, + previousFrame.pixelPaletteIndex, + previousFrame.frameOpts + ); + } + // previous frame object should now have details of this frame + previousFrame = { + pixelPaletteIndex, + frameOpts + }; + } + + previousFrame.frameOpts.disposal = 1; + // add the last frame + gifWriter.addFrame( + 0, + 0, + pImg.width, + pImg.height, + previousFrame.pixelPaletteIndex, + previousFrame.frameOpts + ); + + const extension = 'gif'; + const blob = new Blob([buffer.slice(0, gifWriter.end())], { + type: 'image/gif' + }); + p5.prototype.downloadFile(blob, filename, extension); +}; + /** * Capture a sequence of frames that can be used to create a movie. * Accepts a callback. For example, you may wish to send the frames From 540b2f2198449a07ef9bd56fb62afd71a1daa86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 4 Aug 2022 10:40:14 +0200 Subject: [PATCH 112/177] uncomment omggif import --- src/image/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image/image.js b/src/image/image.js index 2f69b1c969..52cac37870 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -10,7 +10,7 @@ * for drawing images to the main display canvas. */ import p5 from '../core/main'; -// import omggif from 'omggif'; +import omggif from 'omggif'; /** * Creates a new p5.Image (the datatype for storing images). This provides a From 571f8a52345d2a8ba172e59bded25a6f7c95ec9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 4 Aug 2022 11:51:41 +0200 Subject: [PATCH 113/177] change ecmaVersion to accept async --- Gruntfile.js | 4 ++-- src/image/loading_displaying.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 52f437967c..5ea366e622 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -150,7 +150,7 @@ module.exports = grunt => { source: { options: { parserOptions: { - ecmaVersion: 5 + ecmaVersion: 8 } }, src: ['src/**/*.js'] @@ -163,7 +163,7 @@ module.exports = grunt => { 'eslint-samples': { options: { parserOptions: { - ecmaVersion: 6 + ecmaVersion: 8 }, format: 'unix' }, diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index e3a2e9a6bc..b1b1812150 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -256,7 +256,7 @@ p5.prototype.saveGif = async function(...args) { // ); const lastPixelDensity = this._pixelDensity; - pixelDensity(1); + this.pixelDensity(1); const pd = this._pixelDensity; // width and height based on (p)ixel (d)ensity @@ -269,7 +269,7 @@ p5.prototype.saveGif = async function(...args) { if (document.getElementById('progressBar') !== null) document.getElementById('progressBar').remove(); - let p = createP(''); + let p = this.createP(''); p.id('progressBar'); p.style('font-size', '16px'); @@ -303,8 +303,8 @@ p5.prototype.saveGif = async function(...args) { // console.info('Frames processed, encoding gif. This may take a while...'); p.html('Frames processed, encoding gif. This may take a while...'); - loop(); - pixelDensity(lastPixelDensity); + this.loop(); + this.pixelDensity(lastPixelDensity); // create the gif encoder and the colorspace format const gif = GIFEncoder(); @@ -393,6 +393,7 @@ p5.prototype.saveGif = async function(...args) { frames = []; this.loop(); + p.html('Done. Downloading!🌸'); p5.prototype.downloadFile(blob, fileName, extension); }; From 919cffabd5cc8e41cea6edfacdff894eeb622702 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 4 Aug 2022 21:01:34 -0700 Subject: [PATCH 114/177] Update Internalization steward area name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e109c689d7..af51776078 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | | [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor); [@davepagurek](https://github.com/davepagurek); [@jeffawang](https://github.com/jeffawang); [@AdilRabbani](https://github.com/AdilRabbani) | | Build Process/Unit Testing | [@outofambit](https://github.com/outofambit), [@kungfuchicken](https://github.com/kungfuchicken) | -| Localization Tools | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | +| Internalization | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | | Friendly Errors | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | | [Contributor Docs](https://github.com/processing/p5.js/tree/main/contributor_docs) | [SoD 2022](https://github.com/processing/p5.js/wiki/Season-of-Docs-2022-Organization-Application---p5.js): [@limzykenneth](https://github.com/limzykenneth) | From a9674263c0ac196923bc6057bc92157e945a362a Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 4 Aug 2022 21:11:57 -0700 Subject: [PATCH 115/177] Update Internalization label --- .github/ISSUE_TEMPLATE/existing-feature-enhancement.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml index b549520e81..9d4808d754 100644 --- a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml @@ -28,7 +28,7 @@ body: - label: WebGL - label: Build Process - label: Unit Testing - - label: Localization Tools + - label: Internalization - label: Friendly Errors - label: Other (specify if possible) - type: textarea From 68d3c6414e1cab3f9d410a378da559e70f7b05eb Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 4 Aug 2022 21:13:46 -0700 Subject: [PATCH 116/177] Update Internalization label --- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- .github/ISSUE_TEMPLATE/found-a-bug.yml | 2 +- .github/labeler.yml | 2 +- contributor_docs/issue_labels.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index dbe464acec..dd300bf37c 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -28,7 +28,7 @@ body: - label: WebGL - label: Build Process - label: Unit Testing - - label: Localization Tools + - label: Internalization - label: Friendly Errors - label: Other (specify if possible) - type: textarea diff --git a/.github/ISSUE_TEMPLATE/found-a-bug.yml b/.github/ISSUE_TEMPLATE/found-a-bug.yml index e083a135a0..12cff53559 100644 --- a/.github/ISSUE_TEMPLATE/found-a-bug.yml +++ b/.github/ISSUE_TEMPLATE/found-a-bug.yml @@ -22,7 +22,7 @@ body: - label: WebGL - label: Build Process - label: Unit Testing - - label: Localization Tools + - label: Internalization - label: Friendly Errors - label: Other (specify if possible) - type: input diff --git a/.github/labeler.yml b/.github/labeler.yml index 0e87871f8b..11c53fab04 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -27,6 +27,6 @@ "Unit Testing": - '\[X\] Unit Testing' "Localization": - - '\[X\] Localization Tools' + - '\[X\] Internalization' "Friendly Errors": - '\[X\] Friendly Errors' diff --git a/contributor_docs/issue_labels.md b/contributor_docs/issue_labels.md index bd45e54603..33768978ba 100644 --- a/contributor_docs/issue_labels.md +++ b/contributor_docs/issue_labels.md @@ -41,7 +41,7 @@ All the Area labels are now coordinated with the [src folder](https://github.co All the labels below are coordinated with the rest of the steward [focus areas](https://github.com/processing/p5.js#stewards). * Build Process * Unit Testing -* Localization Tools +* Internalization * Friendly Errors * Documentation From 07670ec668b41da54415ff9323e846239784988a Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Thu, 4 Aug 2022 21:21:40 -0700 Subject: [PATCH 117/177] Update Internalization labeler --- .github/labeler.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 6675b7a63e..2cd071249f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -26,7 +26,7 @@ - '\[[xX]\]\s*Build Process' "Unit Testing": - '\[[xX]\]\s*Unit Testing' -"Localization": - - '\[X\] Internalization' +"Internalization": + - '\[[xX]\]\s*Internalization' "Friendly Errors": - '\[[xX]\]\s*Friendly Errors' From d15cf55ed195e57062fe23337a688d42636c05d6 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 5 Aug 2022 20:12:14 -0400 Subject: [PATCH 118/177] Make reserved word assignment regex work better with function calls --- src/core/friendly_errors/sketch_reader.js | 66 ++++++++++------ test/unit/core/error_helpers.js | 93 +++++++++++++++++++++++ 2 files changed, 136 insertions(+), 23 deletions(-) diff --git a/src/core/friendly_errors/sketch_reader.js b/src/core/friendly_errors/sketch_reader.js index edace305fc..c9c3159577 100644 --- a/src/core/friendly_errors/sketch_reader.js +++ b/src/core/friendly_errors/sketch_reader.js @@ -116,8 +116,43 @@ if (typeof IS_MINIFIED !== 'undefined') { //these regex are used to perform variable extraction //visit https://regexr.com/ for the detailed view - const varName = /(?:(?:let|const|var)\s+)?([\w$]+)/; - const varNameWithComma = /(?:(?:let|const|var)\s+)?([\w$,]+)/; + const optionalVarKeyword = /(?:(?:let|const|var)\s+)?/; + + // Bracketed expressions start with an opening bracket, some amount of non + // bracket characters, then a closing bracket. Note that this won't properly + // parse nested brackets: `constrain(millis(), 0, 1000)` will match + // `constrain(millis()` only, but will still fail gracefully and not try to + // mistakenly read any subsequent code as assignment expressions. + const roundBracketedExpr = /(?:\([^)]*\))/; + const squareBracketedExpr = /(?:\[[^\]]*\])/; + const curlyBracketedExpr = /(?:\{[^}]*\})/; + const bracketedExpr = new RegExp( + [roundBracketedExpr, squareBracketedExpr, curlyBracketedExpr] + .map(regex => regex.source) + .join('|') + ); + + // In an a = b expression, `b` can be any character up to a newline or comma, + // unless the comma is inside of a bracketed expression of some kind (to make + // sure we parse function calls with multiple arguments properly.) + const rightHandSide = new RegExp('(?:' + bracketedExpr.source + '|[^\\n,])+'); + + const leftHandSide = /([\w$]+)/; + const assignmentOperator = /\s*=\s*/; + const singleAssignment = new RegExp( + leftHandSide.source + assignmentOperator.source + rightHandSide.source + ); + const listSeparator = /,\s*/; + const oneOrMoreAssignments = new RegExp( + '(?:' + + singleAssignment.source + + listSeparator.source + + ')*' + + singleAssignment.source + ); + const assignmentStatement = new RegExp( + '^' + optionalVarKeyword.source + oneOrMoreAssignments.source + ); const letConstName = /(?:(?:let|const)\s+)([\w$]+)/; /** @@ -133,27 +168,12 @@ if (typeof IS_MINIFIED !== 'undefined') { //extract variable names from the user's code let matches = []; linesArray.forEach(ele => { - if (ele.includes(',')) { - matches.push( - ...ele.split(',').flatMap(s => { - //below RegExps extract a, b, c from let/const a=10, b=20, c; - //visit https://regexr.com/ for the detailed view. - let match; - if (s.includes('=')) { - match = s.match(/(\w+)\s*(?==)/i); - if (match !== null) return match[1]; - } else if (!s.match(new RegExp('[[]{}]'))) { - let m = s.match(varName); - if (m !== null) return s.match(varNameWithComma)[1]; - } else return []; - }) - ); - } else { - //extract a from let/const a=10; - //visit https://regexr.com/ for the detailed view. - const match = ele.match(letConstName); - if (match !== null) matches.push(match[1]); - } + // Match 0 is the part of the line of code that the regex looked at. + // Matches 1 and onward will be only the variable names on the left hand + // side of assignment expressions. + const match = ele.match(assignmentStatement); + if (!match) return; + matches.push(...match.slice(1).filter(group => group !== undefined)); }); //check if the obtained variables are a part of p5.js or not checkForConstsAndFuncs(matches); diff --git a/test/unit/core/error_helpers.js b/test/unit/core/error_helpers.js index 2f5f4b4101..e7a69eb6a2 100644 --- a/test/unit/core/error_helpers.js +++ b/test/unit/core/error_helpers.js @@ -974,4 +974,97 @@ suite('Tests for p5.js sketch_reader', function() { }); } ); + + testUnMinified( + 'detects reassignment of p5.js functions in declaration lists', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + ['function setup() {', 'let x = 2, text = 2;', '}'], + resolve + ); + }).then(function() { + assert.strictEqual(log.length, 1); + assert.match(log[0], /you have used a p5.js reserved function/); + }); + } + ); + + testUnMinified( + 'detects reassignment of p5.js functions in declaration lists after function calls', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + [ + 'function setup() {', + 'let x = constrain(frameCount, 0, 1000), text = 2;', + '}' + ], + resolve + ); + }).then(function() { + assert.strictEqual(log.length, 1); + assert.match(log[0], /you have used a p5.js reserved function/); + }); + } + ); + + testUnMinified( + 'ignores p5.js functions used in the right hand side of assignment expressions', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + // This will still log an error, as `text` isn't being used correctly + // here, but the important part is that it doesn't say that we're + // trying to reassign a reserved function. + ['function draw() {', 'let x = constrain(100, 0, text);', '}'], + resolve + ); + }).then(function() { + assert.ok( + !log.some(line => + line.match(/you have used a p5.js reserved function/) + ) + ); + }); + } + ); + + testUnMinified( + 'ignores p5.js function names used as function arguments', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + ['function draw() {', 'let myLog = (text) => print(text);', '}'], + resolve + ); + }).then(function() { + assert.strictEqual(log.length, 0); + }); + } + ); + + testUnMinified( + 'fails gracefully on inputs too complicated to parse', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + // This technically is redefining text, but it should stop parsing + // after the double nested brackets rather than try and possibly + // give a false positive error. This particular assignment will get + // caught at runtime regardless by + // `_createFriendlyGlobalFunctionBinder`. + [ + 'function draw() {', + 'let x = constrain(millis(), 0, text = 100)', + '}' + ], + resolve + ); + }).then(function() { + console.log(log); + assert.strictEqual(log.length, 0); + }); + } + ); }); From 73acfb183744179ea03a5f8f8e6bd1778205b6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sat, 6 Aug 2022 10:39:32 +0200 Subject: [PATCH 119/177] renamed functions across the repo --- lib/empty-example/sketch.js | 1 + src/image/image.js | 2 ++ src/image/loading_displaying.js | 18 ++++-------------- src/image/p5.Image.js | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 7a53b24409..d9e79ed6b5 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -102,6 +102,7 @@ function setup() { gen_transparency = random(20, 255); background(24); + // saveGif('mySketch', 2); } function draw() { diff --git a/src/image/image.js b/src/image/image.js index 6d5c9adb3a..d2f0d82c99 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -185,6 +185,8 @@ p5.prototype.saveCanvas = function() { }; // this is the old saveGif, left here for compatibility purposes +// the only place I found it being used was on image/p5.Image.js, on the +// save function. that has been changed to use this function. p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const props = pImg.gifProperties; diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index b1b1812150..d10a79b786 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -251,17 +251,8 @@ p5.prototype.saveGif = async function(...args) { // we start on the frame set by the delay argument frameCount = nFramesDelay; - // console.info( - // 'Processing ' + nFrames + ' frames with ' + delay + ' seconds of delay...' - // ); - const lastPixelDensity = this._pixelDensity; this.pixelDensity(1); - const pd = this._pixelDensity; - - // width and height based on (p)ixel (d)ensity - const width_pd = this.width * pd; - const height_pd = this.height * pd; // We first take every frame that we are going to use for the animation let frames = []; @@ -286,7 +277,7 @@ p5.prototype.saveGif = async function(...args) { */ this.redraw(); - const data = this.drawingContext.getImageData(0, 0, width_pd, height_pd) + const data = this.drawingContext.getImageData(0, 0, this.width, this.height) .data; frames.push(data); @@ -300,7 +291,6 @@ p5.prototype.saveGif = async function(...args) { ); await new Promise(resolve => setTimeout(resolve, 0)); } - // console.info('Frames processed, encoding gif. This may take a while...'); p.html('Frames processed, encoding gif. This may take a while...'); this.loop(); @@ -317,7 +307,7 @@ p5.prototype.saveGif = async function(...args) { for (let i = 0; i < frames.length; i++) { if (i === 0) { const indexedFrame = applyPalette(frames[i], globalPalette, { format }); - gif.writeFrame(indexedFrame, width_pd, height_pd, { + gif.writeFrame(indexedFrame, this.width, this.height, { palette: globalPalette, delay: 20, dispose: 1 @@ -354,7 +344,7 @@ p5.prototype.saveGif = async function(...args) { const indexedFrame = applyPalette(currFramePixels, globalPalette, { format }); - const transparentIndex = currFramePixels[matchingPixelsInFrames[0]]; + const transparentIndex = indexedFrame[matchingPixelsInFrames[0]]; for (let mp of matchingPixelsInFrames) { // here, we overwrite whatever color this pixel was assigned to @@ -366,7 +356,7 @@ p5.prototype.saveGif = async function(...args) { } // Write frame into the encoder - gif.writeFrame(indexedFrame, width_pd, height_pd, { + gif.writeFrame(indexedFrame, this.width, this.height, { delay: 20, transparent: true, transparentIndex: transparentIndex, diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 97253ec1ad..d0a6b469e3 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -890,7 +890,7 @@ p5.Image.prototype.isModified = function() { */ p5.Image.prototype.save = function(filename, extension) { if (this.gifProperties) { - p5.prototype.saveGif(this, filename); + p5.prototype.encodeAndDownloadGif(this, filename); } else { p5.prototype.saveCanvas(this.canvas, filename, extension); } From ae03db0c62863387933f5dfd88ed3cb7dfbde411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 8 Aug 2022 12:28:04 +0200 Subject: [PATCH 120/177] better global palette and fixed index bug! --- src/image/loading_displaying.js | 56 ++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index d10a79b786..8ccc3c4eca 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -302,6 +302,7 @@ p5.prototype.saveGif = async function(...args) { // calculate the global palette for this set of frames const globalPalette = _generateGlobalPalette(frames, format); + const transparentIndex = globalPalette.length - 1; // we are going to iterate the frames in pairs, n-1 and n for (let i = 0; i < frames.length; i++) { @@ -344,7 +345,8 @@ p5.prototype.saveGif = async function(...args) { const indexedFrame = applyPalette(currFramePixels, globalPalette, { format }); - const transparentIndex = indexedFrame[matchingPixelsInFrames[0]]; + + // console.log(transparentIndex, globalPalette[transparentIndex]); for (let mp of matchingPixelsInFrames) { // here, we overwrite whatever color this pixel was assigned to @@ -384,7 +386,7 @@ p5.prototype.saveGif = async function(...args) { frames = []; this.loop(); - p.html('Done. Downloading!🌸'); + p.html('Done. Downloading your gif!🌸'); p5.prototype.downloadFile(blob, fileName, extension); }; @@ -396,16 +398,30 @@ function _generateGlobalPalette(frames, format) { // calculate the frequency table for the colors let colorFreq = {}; - for (let f of frames) { - let currPalette = quantize(f, 64, { format }); - for (let color of currPalette) { + for (let f = 0; f < frames.length; f++) { + /** + * here, we use the quantize function in a rather unusual way. + * the quantize function will return a subset of colors for + * the given array of pixels. this is kinda like a "sum up" of + * the most important colors in the image, which will prevent us + * from exhaustively analyzing every pixel from every frame. + * + * in this case, we can just analyze the subset of the most + * important colors from each frame, which is actually more + * than enough for it to work properly. + */ + let currPalette = quantize(frames[f], 256, { format }); + + for (let c = 0; c < currPalette.length; c++) { // colors are in the format [r, g, b, (a)], as in [255, 127, 45, 255] // we'll convert the array to its string representation so it can be used as an index! - color = color.toString(); - if (colorFreq[color] === undefined) { - colorFreq[color] = { count: 1 }; + + let colorStr = currPalette[c].toString(); + + if (colorFreq[colorStr] === undefined) { + colorFreq[colorStr] = 1; } else { - colorFreq[color].count += 1; + colorFreq[colorStr] = colorFreq[colorStr] + 1; } } } @@ -413,14 +429,24 @@ function _generateGlobalPalette(frames, format) { // at this point colorFreq is a dict with {color: count}, // telling us how many times each color appears in the whole animation + // we create a new view into the dictionary as an array, in the form + // ['color', count] + let dictItems = Object.keys(colorFreq).map(function(key) { + return [key, colorFreq[key]]; + }); + + // with that view, we can now properly sort the array based + // on the second component of each element + dictItems.sort(function(first, second) { + return second[1] - first[1]; + }); + // we process it undoing the string operation coverting that into - // an array of strings (['255', '127', '45', '255']) and then we convert + // an array of strings (['255', '127', '45']) and then we convert // that again to an array of integers - let colorsSortedByFreq = Object.keys(colorFreq) - .sort((a, b) => { - return colorFreq[b].count - colorFreq[a].count; - }) - .map(c => c.split(',').map(x => parseInt(x))); + let colorsSortedByFreq = dictItems.map(i => + i[0].split(',').map(n => parseInt(n)) + ); // now we simply extract the top 256 colors! return colorsSortedByFreq.splice(0, 256); From 629d7448d4dc0cf562bc9214ab6cc76d04f5a680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 8 Aug 2022 13:01:28 +0200 Subject: [PATCH 121/177] adjust test from saveGif to encodeAndDownloadGif --- test/unit/image/downloading.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/image/downloading.js b/test/unit/image/downloading.js index e4973151e8..19b6caf905 100644 --- a/test/unit/image/downloading.js +++ b/test/unit/image/downloading.js @@ -32,16 +32,16 @@ suite('downloading animated gifs', function() { }); }); - suite('p5.prototype.saveGif', function() { + suite('p5.prototype.encodeAndDownloadGif', function() { test('should be a function', function() { - assert.ok(myp5.saveGif); - assert.typeOf(myp5.saveGif, 'function'); + assert.ok(myp5.encodeAndDownloadGif); + assert.typeOf(myp5.encodeAndDownloadGif, 'function'); }); test('should not throw an error', function() { - myp5.saveGif(myGif); + myp5.encodeAndDownloadGif(myGif); }); testWithDownload('should download a gif', function(blobContainer) { - myp5.saveGif(myGif); + myp5.encodeAndDownloadGif(myGif); let gifBlob = blobContainer.blob; assert.strictEqual(gifBlob.type, 'image/gif'); }); From 0ebf9b712d609132511fde841d314620bef315c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 8 Aug 2022 13:53:04 +0200 Subject: [PATCH 122/177] small styling change --- src/image/loading_displaying.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 8ccc3c4eca..840da6f5cb 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -266,6 +266,8 @@ p5.prototype.saveGif = async function(...args) { p.style('font-size', '16px'); p.style('font-family', 'Montserrat'); p.style('background-color', '#ffffffa0'); + p.style('padding', '8px'); + p.style('border-radius', '10px'); p.position(0, 0); while (count < nFrames + nFramesDelay) { From 566474400be8c4293f0abbcf6ae6a87e015bcaf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 8 Aug 2022 19:28:14 +0200 Subject: [PATCH 123/177] added support for WEBGL renderer! --- src/image/loading_displaying.js | 62 +++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 840da6f5cb..6ffb029d21 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -270,6 +270,15 @@ p5.prototype.saveGif = async function(...args) { p.style('border-radius', '10px'); p.position(0, 0); + let pixels; + let gl; + if (this.drawingContext instanceof WebGLRenderingContext) { + // if we have a WEBGL context, initialize the pixels array + // and the gl context to use them inside the loop + gl = document.getElementById('defaultCanvas0').getContext('webgl'); + pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); + } + while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -279,8 +288,29 @@ p5.prototype.saveGif = async function(...args) { */ this.redraw(); - const data = this.drawingContext.getImageData(0, 0, this.width, this.height) - .data; + // depending on the context we'll extract the pixels one way + // or another + let data = undefined; + + if (this.drawingContext instanceof WebGLRenderingContext) { + pixels = new Uint8Array( + gl.drawingBufferWidth * gl.drawingBufferHeight * 4 + ); + gl.readPixels( + 0, + 0, + gl.drawingBufferWidth, + gl.drawingBufferHeight, + gl.RGBA, + gl.UNSIGNED_BYTE, + pixels + ); + + data = _flipPixels(pixels); + } else { + data = this.drawingContext.getImageData(0, 0, this.width, this.height) + .data; + } frames.push(data); count++; @@ -392,6 +422,34 @@ p5.prototype.saveGif = async function(...args) { p5.prototype.downloadFile(blob, fileName, extension); }; +function _flipPixels(pixels) { + // extracting the pixels using readPixels returns + // an upside down image. we have to flip it back + // first. this solution is proposed by gman on + // this stack overflow answer: + // https://stackoverflow.com/questions/41969562/how-can-i-flip-the-result-of-webglrenderingcontext-readpixels + + var halfHeight = parseInt(height / 2); + var bytesPerRow = width * 4; + + // make a temp buffer to hold one row + var temp = new Uint8Array(width * 4); + for (var y = 0; y < halfHeight; ++y) { + var topOffset = y * bytesPerRow; + var bottomOffset = (height - y - 1) * bytesPerRow; + + // make copy of a row on the top half + temp.set(pixels.subarray(topOffset, topOffset + bytesPerRow)); + + // copy a row from the bottom half to the top + pixels.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow); + + // copy the copy of the top half row to the bottom half + pixels.set(temp, bottomOffset); + } + return pixels; +} + function _generateGlobalPalette(frames, format) { // for each frame, we'll keep track of the count of // every unique color. that is: how many times does From 02c44be28c3eccc942026e2d767bb602fbb27d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Tue, 9 Aug 2022 17:52:42 +0200 Subject: [PATCH 124/177] created new globalPalette function --- src/image/loading_displaying.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 6ffb029d21..c902b6769c 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -334,6 +334,8 @@ p5.prototype.saveGif = async function(...args) { // calculate the global palette for this set of frames const globalPalette = _generateGlobalPalette(frames, format); + // const globalPalette2 = _generateGlobalPalette2(frames, format); + console.log(globalPalette); const transparentIndex = globalPalette.length - 1; // we are going to iterate the frames in pairs, n-1 and n @@ -392,8 +394,8 @@ p5.prototype.saveGif = async function(...args) { gif.writeFrame(indexedFrame, this.width, this.height, { delay: 20, - transparent: true, - transparentIndex: transparentIndex, + // transparent: true, + // transparentIndex: transparentIndex, dispose: 1 }); @@ -450,6 +452,24 @@ function _flipPixels(pixels) { return pixels; } +// function _generateGlobalPalette2(frames, format) { +// // make an array the size of every possible color in every possible frame +// // that is: width * height * frames. +// let allColors = new Uint8Array(frames.length * frames[0].length); + +// // put every frame one after the other in sequence. +// // this array will hold absolutely every pixel from the animation. +// // the set function on the Uint8Array works super fast tho! +// for (let f = 0; f < frames.length; f++) { +// allColors.set(frames[0], f * frames[0].length); +// } + +// // quantize this massive array into 256 colors and return it! +// let colorPalette = quantize(allColors, 255, { format }); +// colorPalette.push([-1, -1, -1]); +// return colorPalette; +// } + function _generateGlobalPalette(frames, format) { // for each frame, we'll keep track of the count of // every unique color. that is: how many times does @@ -508,6 +528,7 @@ function _generateGlobalPalette(frames, format) { i[0].split(',').map(n => parseInt(n)) ); + console.log(colorsSortedByFreq.splice(0, 256)); // now we simply extract the top 256 colors! return colorsSortedByFreq.splice(0, 256); } From 7557cf8024dcfe34bdae64f489a99572f87ad763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 10 Aug 2022 13:36:13 +0200 Subject: [PATCH 125/177] refactoring and better palette generation --- src/image/loading_displaying.js | 144 ++++++++++++-------------------- 1 file changed, 55 insertions(+), 89 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index c902b6769c..8b0acecff0 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -323,25 +323,26 @@ p5.prototype.saveGif = async function(...args) { ); await new Promise(resolve => setTimeout(resolve, 0)); } - p.html('Frames processed, encoding gif. This may take a while...'); + p.html('Frames processed, generating color palette...'); this.loop(); this.pixelDensity(lastPixelDensity); // create the gif encoder and the colorspace format const gif = GIFEncoder(); - const format = 'rgb444'; // calculate the global palette for this set of frames - const globalPalette = _generateGlobalPalette(frames, format); - // const globalPalette2 = _generateGlobalPalette2(frames, format); - console.log(globalPalette); + const globalPalette = _generateGlobalPalette(frames); + + // the way we designed the palette means we always take the last index for transparency const transparentIndex = globalPalette.length - 1; // we are going to iterate the frames in pairs, n-1 and n for (let i = 0; i < frames.length; i++) { if (i === 0) { - const indexedFrame = applyPalette(frames[i], globalPalette, { format }); + const indexedFrame = applyPalette(frames[i], globalPalette, { + format: 'rgba4444' + }); gif.writeFrame(indexedFrame, this.width, this.height, { palette: globalPalette, delay: 20, @@ -353,7 +354,7 @@ p5.prototype.saveGif = async function(...args) { // matching pixels between frames can be set to full transparency, // kinda digging a "hole" into the frame to see the pixels that where behind it // (which would be the exact same, so not noticeable changes) - // this helps make the file smaller + // this helps make the file quite smaller let currFramePixels = frames[i]; let lastFramePixels = frames[i - 1]; let matchingPixelsInFrames = []; @@ -370,6 +371,8 @@ p5.prototype.saveGif = async function(...args) { lastFramePixels[p + 2], lastFramePixels[p + 3] ]; + + // if the pixels are equal, save this index to be used later if (_pixelEquals(currPixel, lastPixel)) { matchingPixelsInFrames.push(parseInt(p / 4)); } @@ -377,25 +380,24 @@ p5.prototype.saveGif = async function(...args) { // we decide on one of this colors to be fully transparent // Apply palette to RGBA data to get an indexed bitmap const indexedFrame = applyPalette(currFramePixels, globalPalette, { - format + format: 'rgba4444' }); - // console.log(transparentIndex, globalPalette[transparentIndex]); - - for (let mp of matchingPixelsInFrames) { + for (let i = 0; i < matchingPixelsInFrames.length; i++) { // here, we overwrite whatever color this pixel was assigned to // with the color that we decided we are going to use as transparent. // down in writeFrame we are going to tell the encoder that whenever // it runs into "transparentIndex", just dig a hole there allowing to // see through what was in the frame before it. - indexedFrame[mp] = transparentIndex; + let pixelIndex = matchingPixelsInFrames[i]; + indexedFrame[pixelIndex] = transparentIndex; } - // Write frame into the encoder + // Write frame into the encoder gif.writeFrame(indexedFrame, this.width, this.height, { delay: 20, - // transparent: true, - // transparentIndex: transparentIndex, + transparent: true, + transparentIndex: transparentIndex, dispose: 1 }); @@ -452,85 +454,49 @@ function _flipPixels(pixels) { return pixels; } -// function _generateGlobalPalette2(frames, format) { -// // make an array the size of every possible color in every possible frame -// // that is: width * height * frames. -// let allColors = new Uint8Array(frames.length * frames[0].length); - -// // put every frame one after the other in sequence. -// // this array will hold absolutely every pixel from the animation. -// // the set function on the Uint8Array works super fast tho! -// for (let f = 0; f < frames.length; f++) { -// allColors.set(frames[0], f * frames[0].length); -// } - -// // quantize this massive array into 256 colors and return it! -// let colorPalette = quantize(allColors, 255, { format }); -// colorPalette.push([-1, -1, -1]); -// return colorPalette; -// } - -function _generateGlobalPalette(frames, format) { - // for each frame, we'll keep track of the count of - // every unique color. that is: how many times does - // this particular color appear in every frame? - // Then we'll sort the colors and pick the top 256! - - // calculate the frequency table for the colors - let colorFreq = {}; +function _generateGlobalPalette(frames) { + // make an array the size of every possible color in every possible frame + // that is: width * height * frames. + let allColors = new Uint8Array(frames.length * frames[0].length); + + // put every frame one after the other in sequence. + // this array will hold absolutely every pixel from the animation. + // the set function on the Uint8Array works super fast tho! for (let f = 0; f < frames.length; f++) { - /** - * here, we use the quantize function in a rather unusual way. - * the quantize function will return a subset of colors for - * the given array of pixels. this is kinda like a "sum up" of - * the most important colors in the image, which will prevent us - * from exhaustively analyzing every pixel from every frame. - * - * in this case, we can just analyze the subset of the most - * important colors from each frame, which is actually more - * than enough for it to work properly. - */ - let currPalette = quantize(frames[f], 256, { format }); - - for (let c = 0; c < currPalette.length; c++) { - // colors are in the format [r, g, b, (a)], as in [255, 127, 45, 255] - // we'll convert the array to its string representation so it can be used as an index! - - let colorStr = currPalette[c].toString(); - - if (colorFreq[colorStr] === undefined) { - colorFreq[colorStr] = 1; - } else { - colorFreq[colorStr] = colorFreq[colorStr] + 1; - } - } + allColors.set(frames[0], f * frames[0].length); } - // at this point colorFreq is a dict with {color: count}, - // telling us how many times each color appears in the whole animation - - // we create a new view into the dictionary as an array, in the form - // ['color', count] - let dictItems = Object.keys(colorFreq).map(function(key) { - return [key, colorFreq[key]]; - }); - - // with that view, we can now properly sort the array based - // on the second component of each element - dictItems.sort(function(first, second) { - return second[1] - first[1]; + // quantize this massive array into 256 colors and return it! + let colorPalette = quantize(allColors, 256, { + format: 'rgba444', + oneBitAlpha: true }); - // we process it undoing the string operation coverting that into - // an array of strings (['255', '127', '45']) and then we convert - // that again to an array of integers - let colorsSortedByFreq = dictItems.map(i => - i[0].split(',').map(n => parseInt(n)) - ); - - console.log(colorsSortedByFreq.splice(0, 256)); - // now we simply extract the top 256 colors! - return colorsSortedByFreq.splice(0, 256); + // when generating the palette, we have to leave space for 1 of the + // indices to be a random color that does not appear anywhere in our + // animation to use for transparency purposes. So, if the palette is full + // (has 256 colors), we overwrite the last one with a random, fully transparent + // color. Otherwise, we just push a new color into the palette the same way. + + // this guarantees that when using the transparency index, there are no matches + // between some colors of the animation and the "holes" we want to dig on them, + // which would cause pieces of some frames to be transparent and thus look glitchy. + if (colorPalette.length === 256) { + colorPalette[colorPalette.length - 1] = [ + Math.random() * 255, + Math.random() * 255, + Math.random() * 255, + 0 + ]; + } else { + colorPalette.push([ + Math.random() * 255, + Math.random() * 255, + Math.random() * 255, + 0 + ]); + } + return colorPalette; } function _pixelEquals(a, b) { From bed8cfa01c94c03c49757b5ea8ad3f4ce2e93efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 11 Aug 2022 12:59:19 +0200 Subject: [PATCH 126/177] refactor arguments and support for all frameRates! --- src/image/loading_displaying.js | 38 ++++++++++----------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 8b0acecff0..963c398e93 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -207,31 +207,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * @alt * animation of a circle moving smoothly diagonally */ -p5.prototype.saveGif = async function(...args) { - // process args - - let fileName; - let seconds; - let delay; - - // this section takes care of parsing and processing - // the arguments in the correct format - switch (args.length) { - case 2: - fileName = args[0]; - seconds = args[1]; - break; - case 3: - fileName = args[0]; - seconds = args[1]; - delay = args[2]; - break; - } - - if (!delay) { - delay = 0; - } - +p5.prototype.saveGif = async function(fileName, seconds = 3, delay = 0) { // get the project's framerate // if it is undefined or some non useful value, assume it's 60 let _frameRate = this._targetFrameRate; @@ -239,6 +215,14 @@ p5.prototype.saveGif = async function(...args) { _frameRate = 60; } + // calculate delay based on frameRate + let gifFrameDelay = 1 / _frameRate * 1000; + + // constrain it to be always greater than 20, + // otherwise it won't work in some browsers and systems + // reference: https://stackoverflow.com/questions/64473278/gif-frame-duration-seems-slower-than-expected + gifFrameDelay = gifFrameDelay < 20 ? 20 : gifFrameDelay; + // because the input was in seconds, we now calculate // how many frames those seconds translate to let nFrames = Math.ceil(seconds * _frameRate); @@ -345,7 +329,7 @@ p5.prototype.saveGif = async function(...args) { }); gif.writeFrame(indexedFrame, this.width, this.height, { palette: globalPalette, - delay: 20, + delay: gifFrameDelay, dispose: 1 }); continue; @@ -395,7 +379,7 @@ p5.prototype.saveGif = async function(...args) { // Write frame into the encoder gif.writeFrame(indexedFrame, this.width, this.height, { - delay: 20, + delay: gifFrameDelay, transparent: true, transparentIndex: transparentIndex, dispose: 1 From 79452ecff64d11f14d73a3c254cba25db896628b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 11 Aug 2022 13:55:50 +0200 Subject: [PATCH 127/177] change typing in documentation example --- src/image/loading_displaying.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 963c398e93..a1093e5f59 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -171,8 +171,8 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * * @method saveGif * @param {String} filename File name of your gif - * @param {String} duration Duration in seconds that you wish to capture from your sketch - * @param {String} delay Duration in seconds that you wish to wait before starting to capture + * @param {Number} duration Duration in seconds that you wish to capture from your sketch + * @param {Number} delay Duration in seconds that you wish to wait before starting to capture * * @example *
From 5b582000403012633c73461182a912bb11ad7271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 11 Aug 2022 13:56:50 +0200 Subject: [PATCH 128/177] add parameter validation through FES --- src/image/loading_displaying.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index a1093e5f59..c52c7dbc96 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -208,6 +208,8 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * animation of a circle moving smoothly diagonally */ p5.prototype.saveGif = async function(fileName, seconds = 3, delay = 0) { + // validate parameters + p5._validateParameters('saveGif', arguments); // get the project's framerate // if it is undefined or some non useful value, assume it's 60 let _frameRate = this._targetFrameRate; From 547b533d0c73dfa0bd3d81adeda832f8f9b3cd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Thu, 11 Aug 2022 13:56:58 +0200 Subject: [PATCH 129/177] wrote initial tests --- test/unit/image/downloading.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/unit/image/downloading.js b/test/unit/image/downloading.js index 19b6caf905..67419dfe02 100644 --- a/test/unit/image/downloading.js +++ b/test/unit/image/downloading.js @@ -320,3 +320,33 @@ suite('p5.prototype.saveFrames', function() { }); }); }); + +suite('p5.prototype.saveGif', function() { + setup(function(done) { + new p5(function(p) { + p.setup = function() { + myp5 = p; + p.createCanvas(10, 10); + done(); + }; + }); + }); + + teardown(function() { + myp5.remove(); + }); + + test('should be a function', function() { + assert.ok(myp5.saveGif); + assert.typeOf(myp5.saveGif, 'function'); + }); + test('should not throw an error', function() { + myp5.saveGif('myGif', 3, 2); + }); + testWithDownload('should download a GIF', async function(blobContainer) { + myp5.saveGif(myGif, 3, 2); + await waitForBlob(blobContainer); + let gifBlob = blobContainer.blob; + assert.strictEqual(gifBlob.type, 'image/gif'); + }); +}); From 57158e5ae5aaabaf0cca6bebfa4e9a2248135cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 17 Aug 2022 09:26:44 +0200 Subject: [PATCH 130/177] fix use of var in line 234 --- src/image/loading_displaying.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index c52c7dbc96..3fe49dd81e 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -231,7 +231,7 @@ p5.prototype.saveGif = async function(fileName, seconds = 3, delay = 0) { let nFramesDelay = Math.ceil(delay * _frameRate); // initialize variables for the frames processing - var count = nFramesDelay; + let count = nFramesDelay; this.noLoop(); // we start on the frame set by the delay argument @@ -360,7 +360,7 @@ p5.prototype.saveGif = async function(fileName, seconds = 3, delay = 0) { // if the pixels are equal, save this index to be used later if (_pixelEquals(currPixel, lastPixel)) { - matchingPixelsInFrames.push(parseInt(p / 4)); + matchingPixelsInFrames.push(p / 4); } } // we decide on one of this colors to be fully transparent From 5fd17662403afc7c974b3ecce9d757306c68590d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Wed, 17 Aug 2022 09:28:25 +0200 Subject: [PATCH 131/177] remove initialization of arguments --- src/image/loading_displaying.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 3fe49dd81e..48d991aba1 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -207,9 +207,10 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * @alt * animation of a circle moving smoothly diagonally */ -p5.prototype.saveGif = async function(fileName, seconds = 3, delay = 0) { +p5.prototype.saveGif = async function(fileName, seconds, delay) { // validate parameters p5._validateParameters('saveGif', arguments); + // get the project's framerate // if it is undefined or some non useful value, assume it's 60 let _frameRate = this._targetFrameRate; From 6c9c6fc835a9f8974020a13c4fcf276f67a58435 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 17 Aug 2022 10:38:41 +0100 Subject: [PATCH 132/177] Initial restructure of contributor README.md --- contributor_docs/README.md | 47 +++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/contributor_docs/README.md b/contributor_docs/README.md index de7f4ababe..ebeb379e95 100644 --- a/contributor_docs/README.md +++ b/contributor_docs/README.md @@ -2,13 +2,48 @@ # 🌸 Welcome! 🌺 -Thanks for your interest in contributing to p5.js! Our community values contributions of all forms and seeks to expand the meaning of the word "contributor" as far and wide as possible. It includes documentation, teaching, writing code, making art, writing, design, activism, organizing, curating, or anything else you might imagine. [Our community page](https://p5js.org/community/#contribute) gives an overview of some different ways to get involved and contribute. For technical contributions, read on to get started. +Thanks for your interest in contributing to p5.js! Our community values contributions of all forms and seeks to expand the meaning of the word "contributor" as far and wide as possible. It includes documentation, teaching, writing code, making art, writing, design, activism, organizing, curating, or anything else you might imagine. [Our community page](https://p5js.org/community/#contribute) gives an overview of some different ways to get involved and contribute. -This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Add yourself to the [readme](https://github.com/processing/p5.js/blob/main/README.md#contributors) by following the [instructions here](https://github.com/processing/p5.js/issues/2309)! Or comment in the [GitHub issues](https://github.com/processing/p5.js/issues) with your contribution and we'll add you. The contributor docs are published on p5.js [website](https://p5js.org/contributor-docs/#/), and hosted on p5.js [GitHub repository](https://github.com/processing/p5.js/tree/main/contributor_docs). +This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. We use the @all-contributors bot to handle adding people to the README.md file. You can ask @all-contributors bot to add you in an issue or PR comment like so: +``` + +``` +Although we will usually automatically add you to the contributor list using the bot after merging your PR. The contributor docs are published on p5.js [website](https://p5js.org/contributor-docs/#/), and hosted on p5.js [GitHub repository](https://github.com/processing/p5.js/tree/main/contributor_docs). + +# Before Contributing +Contributing to p5.js should be a stress free experience and we welcome contributions of all levels, whether you are just fixing a small typo in the documentation or refactoring complex 3D rendering functionalities. However there are just a few things you should be familiar with before starting your contribution. + +First, please have a read through our [community statement](https://p5js.org/community/). + +Next, we are currently prioritizing work that expands access (inclusion and accessibility) to p5.js! See [our access statement](./access.md) for more details. -# Prioritizing access +# Get Started +Now you are ready to start contributing to p5.js! There are many ways to get started with contributing to p5.js and many reasons to do so. For the purpose of this documentation, we will split contributions roughly into two categories. +- Contributions that directly deals with the source code (including documentation) +- Contributions that directly deals with the source code very little or not at all -We are prioritizing work that expands access (inclusion and accessibility) to p5.js! See [our access statement](./access.md) for more details. +Depending on what kind of contribution you are making to p5.js, please read on to the relevant section of this documentation. + +## Source code contribution +For a typical contribution to the p5.js or p5.js-website repository, we will follow the following steps: +1. Open an issue +2. Discuss +3. Approved for opening a Pull Request (PR) +4. Make necessary changes +5. Open a PR +6. Discuss +7. Approved and merged + +## Non-source code contribution +There are many more ways to contribute to p5.js through non-source code contribution than can be exhaustively list here, some of the ways may also involve working with some of the p5.js repositories (such as adding example, writing tutorial for the website, etc). Depending on what the planned contribution is, we may be able to support you in different ways so do reach out to us via any channel available to you (email, social media, Discourse forum, Discord, etc). + +## Stewards and maintainers +This section links to different topics related to the general maintenance of p5.js' repositories. +- Responding to issues and reviewing PRs +- How the library is built +- Releasing a new version + +--- # Where our code lives @@ -17,8 +52,8 @@ The overarching p5.js project includes some repositories other than this one: - **[p5.js](https://github.com/processing/p5.js)**: This repository contains the source code for the p5.js library. The [user-facing p5.js reference manual](https://p5js.org/reference/) is also generated from the [JSDoc](https://jsdoc.app/) comments included in this source code. It is maintained by [Qianqian Ye](https://github.com/qianqianye) and a group of [stewards](https://github.com/processing/p5.js#stewards). - **[p5.js-website](https://github.com/processing/p5.js-website)**: This repository contains most of the code for the [p5.js website](http://p5js.org), with the exception of the reference manual. It is maintained by [Qianqian Ye](https://github.com/qianqianye), [Kenneth Lim](https://github.com/limzykenneth), and a group of [stewards](https://github.com/processing/p5.js-website#stewards). - **[p5.js-sound](https://github.com/processing/p5.js-sound)**: This repository contains the p5.sound.js library. It is maintained by [Jason Sigal](https://github.com/therewasaguy). -- **[p5.js-web-editor](https://github.com/processing/p5.js-web-editor)**: This repository contains the source code for the [p5.js web editor](https://editor.p5js.org). It is maintained by [Cassie Tarakajian](https://github.com/catarak). Note that the older [p5.js editor](https://github.com/processing/p5.js-editor) is now deprecated. - +- **[p5.js-web-editor](https://github.com/processing/p5.js-web-editor)**: This repository contains the source code for the [p5.js web editor](https://editor.p5js.org). It is maintained by [Cassie Tarakajian](https://github.com/catarak). +- Other add-on libraries not listed above usually have their own repository and maintainers and are not maintained by the p5.js project directly. # Repository File Structure From 69b3cadba6477a393530f68c760dc1d048b34cc3 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 18 Aug 2022 11:32:25 +0100 Subject: [PATCH 133/177] Add example for adding self to contributor list --- contributor_docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contributor_docs/README.md b/contributor_docs/README.md index ebeb379e95..70a9c26453 100644 --- a/contributor_docs/README.md +++ b/contributor_docs/README.md @@ -6,9 +6,9 @@ Thanks for your interest in contributing to p5.js! Our community values contribu This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. We use the @all-contributors bot to handle adding people to the README.md file. You can ask @all-contributors bot to add you in an issue or PR comment like so: ``` - +@all-contributors please add @[your github handle] for [your contribution type] ``` -Although we will usually automatically add you to the contributor list using the bot after merging your PR. The contributor docs are published on p5.js [website](https://p5js.org/contributor-docs/#/), and hosted on p5.js [GitHub repository](https://github.com/processing/p5.js/tree/main/contributor_docs). +You can find relevant contribution type [here](https://allcontributors.org/docs/en/emoji-key). Although we will usually automatically add you to the contributor list using the bot after merging your PR. The contributor docs are published on p5.js [website](https://p5js.org/contributor-docs/#/), and hosted on p5.js [GitHub repository](https://github.com/processing/p5.js/tree/main/contributor_docs). # Before Contributing Contributing to p5.js should be a stress free experience and we welcome contributions of all levels, whether you are just fixing a small typo in the documentation or refactoring complex 3D rendering functionalities. However there are just a few things you should be familiar with before starting your contribution. From dfc545295eb598cd2ab970818638a33391f19207 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 18 Aug 2022 11:42:51 +0100 Subject: [PATCH 134/177] Archvie some outdated contributor docs entries --- contributor_docs/{ => archive}/benchmarking_p5.md | 0 contributor_docs/{ => archive}/discussions.md | 0 contributor_docs/{ => archive}/roadmap.md | 0 contributor_docs/sidebar.md | 3 --- 4 files changed, 3 deletions(-) rename contributor_docs/{ => archive}/benchmarking_p5.md (100%) rename contributor_docs/{ => archive}/discussions.md (100%) rename contributor_docs/{ => archive}/roadmap.md (100%) diff --git a/contributor_docs/benchmarking_p5.md b/contributor_docs/archive/benchmarking_p5.md similarity index 100% rename from contributor_docs/benchmarking_p5.md rename to contributor_docs/archive/benchmarking_p5.md diff --git a/contributor_docs/discussions.md b/contributor_docs/archive/discussions.md similarity index 100% rename from contributor_docs/discussions.md rename to contributor_docs/archive/discussions.md diff --git a/contributor_docs/roadmap.md b/contributor_docs/archive/roadmap.md similarity index 100% rename from contributor_docs/roadmap.md rename to contributor_docs/archive/roadmap.md diff --git a/contributor_docs/sidebar.md b/contributor_docs/sidebar.md index d365f9014c..99ecf5e996 100644 --- a/contributor_docs/sidebar.md +++ b/contributor_docs/sidebar.md @@ -5,15 +5,12 @@ - [Contributing Documentation](contributing_documentation.md) - [Issue Labels](issue_labels.md) - __CONTRIBUTING THOUGHTS__ - - [Discussions](discussions.md) - [Design Principles](design_principles.md) - - [Roadmap](roadmap.md) - [How Contributions are Organized](organization.md) - __CONTRIBUTING CODE__ - [Creating Libraries](creating_libraries.md) - [Opening a Pull Request](preparing_a_pull_request.md) - [Inline Documentation](inline_documentation.md) - - [Benchmarking](benchmarking_p5.md) - [Unit Testing](unit_testing.md) - [Friendly-Error System](friendly_error_system.md) - [Release Process](release_process.md) From 9f1d5024a7614c91c316f45744dc36c2c5955896 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 18 Aug 2022 11:51:03 +0100 Subject: [PATCH 135/177] Archive ES6 adoption documentation --- contributor_docs/{ => archive}/es6-adoption.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contributor_docs/{ => archive}/es6-adoption.md (100%) diff --git a/contributor_docs/es6-adoption.md b/contributor_docs/archive/es6-adoption.md similarity index 100% rename from contributor_docs/es6-adoption.md rename to contributor_docs/archive/es6-adoption.md From b08029b8c07897064228547924e1f9cc09df2ba5 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 18 Aug 2022 13:34:39 +0100 Subject: [PATCH 136/177] Update supported browsers docs to be inline with current practice --- contributor_docs/supported_browsers.md | 31 +++++++------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/contributor_docs/supported_browsers.md b/contributor_docs/supported_browsers.md index 8aa48c74d1..b9e7dde67e 100644 --- a/contributor_docs/supported_browsers.md +++ b/contributor_docs/supported_browsers.md @@ -1,30 +1,15 @@ -#### This page is out of date. Help us update it! See our stated goal below. - # Supported browsers -## Our stated goal: - -We support the current version of the browser, plus the previous major release of the browser. Exceptions: Internet Explorer, which has not had a new major release since 2013, we support only the most recent major release (v.11); Safari, which has not had a new major release since 2015, we support only the most recent major release (v.11) - -## Potential issues: +## Our stated goal +p5.js uses [browserslist](https://browsersl.ist/) and [Babel](https://babeljs.io/) to provide support for older browsers. The browserslist configuration in use is [`last 2 versions, not dead`](https://browserslist.dev/?q=bGFzdCAyIHZlcnNpb25zLCBub3QgZGVhZA%3D%3D). `last 2 versions` means the last two releases of any browsers, `not dead` means browsers that had official support or updates in the past 24 months. Both of these conditions must be true for a browser to be supported. -* We are using webGL, which has limited support in IE 10, Firefox, and the Android browser. -* We are using typed arrays, which does not have support for Uint8ClampedArray in IE 10 and IE Mobile 10/11. -* Canvas blend modes are not supported in IE. -* WebAudio is not supported in IE or the Android Browser. +In practice, you can still use most of the latest features available in Javascript because Babel will likely be able to transpile or polyfill them to something matching the required compatibility list. Some features such as [Web API](https://developer.mozilla.org/en-US/docs/Web/API), [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API), or similar features not part of the core Javascript language cannot be handled by Babel and will need to be assessed on a case by case basis. +Good places to check if a feature is available are [caniuse.com](https://caniuse.com/) and [MDN](https://developer.mozilla.org/en-US/). -As of September 2018, this means that we support: +## Where does this apply +The supported browsers requirement will apply to the p5.js source code, all examples (both website examples page and documentation), and all official tutorials. Third party add-on libraries does not have to adhere to the same requirement but are encouraged to do so. -|Browser | Current Version | Previous Version| Notes -|-------------------|------------------:|----------------:|-------------- -|Internet Explorer | v. 11 | Not supported | No support for WebAudio -|Microsoft Edge | v. 42 | v. 41 | -|Chrome | v. 68 | v. 67 | -|Chrome for Android | v. 68 | v. 67 | -|Firefox | v. 61 | v. 60 | -|Safari | v. 11 | Not supported | -|iOS Safari | v. 11.4 | v. 11.2 | -|Opera | v. 54 | v. 53 | +In many cases browsers not officially supported will likely still work with p5.js but we provide no guarantee for this case. -We will try to list all known problems across the different browsers here but for a complete list of supported feature on a browser visit [caniuse.com](http://caniuse.com) and search for specific features (ie. [WebGL](http://caniuse.com/#search=webgl)) +Stewards of each section will be responsible for ensuring PR involving code changes adhere to this requirement. \ No newline at end of file From 792fcf5d8ee8714e99e75890d1effc1a90dcec96 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 19 Aug 2022 15:38:30 +0100 Subject: [PATCH 137/177] Minor fixes to inaccurate contributor docs --- contributor_docs/inline_documentation.md | 2 +- contributor_docs/organization.md | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/contributor_docs/inline_documentation.md b/contributor_docs/inline_documentation.md index e893a4cc77..7460fd4a59 100644 --- a/contributor_docs/inline_documentation.md +++ b/contributor_docs/inline_documentation.md @@ -4,7 +4,7 @@ By adding inline documentation in the p5.js source code, a reference can be auto See below for the basics, more specifics about yuidoc style [here](http://yui.github.io/yuidoc/syntax/index.html). __Please limit line length to 80 columns, starting new lines when it runs over.__ -__[List of examples needed](https://github.com/processing/p5.js/issues/1954) (you can also view the most up to date list by building the library with grunt and looking at the log messages)__ +__[List of examples needed](https://github.com/processing/p5.js/issues/2865) (you can also view the most up to date list by building the library with grunt and looking at the log messages)__ ## Specify element type and description diff --git a/contributor_docs/organization.md b/contributor_docs/organization.md index f42c008ea6..3558923861 100644 --- a/contributor_docs/organization.md +++ b/contributor_docs/organization.md @@ -1,5 +1,4 @@ # Organizing Contributions - Keeping the repository organized ensures that it is always clear which discussions and tasks are the most important. This helps everyone from maintainers to new contributors navigate the repository without getting overwhelmed. To help with this, we have a set of guidelines for how to organize issues, work, and pull requests. Helping with organization can be a great way to contribute. If a bug report is missing information such as example code, feel free to chime in and ask for the missing info. If an issue with an assignee has seen no activity for 60 days, it can be helpful to comment on the issue to check in with the assignee to see if they still want to work on the issue. Whenever you are helping with organizational tasks, make sure to be kind and always keep the community guidelines in mind. @@ -7,11 +6,12 @@ Helping with organization can be a great way to contribute. If a bug report is m # Guidelines for Organization ## Issues +- **All issues should use the relevant issue template** - **All bug reports should include sample code** - This can be in the form of code posted in the body of the issue, or it can be a link to an online example of the code preferably in [the online editor](https://editor.p5js.org) - **All issues should have at least 2 labels** - This makes it much easier to navigate the issues. - - Try adding a label for the area (webgl, core, image, etc) + - Depending on issue template used, labels for area will be automatically set in many cases. You can add additional labels as required. - **Issue assignment is first-come, first-serve** - If a bug has been reproduced, or a feature request/enhancement has been agreed upon by the community, it becomes available for assignment. When this happens the first contributor to request assignment by saying something like "I'd like to work on this issue!" will be assigned. - Do not request to be assigned to an issue if it is unclear whether the bug is reproducible or the feature request/enhancement has been agreed upon. @@ -24,10 +24,9 @@ Helping with organization can be a great way to contribute. If a bug report is m # Guidelines for Decision-Making - p5 aspires to make its decision-making process as transparent and horizontal as possible. To do this, p5 uses an informal consensus-seeking model for decision-making. This means that we prefer to reach community consensus on any and all decisions. If this fails, then a vote will take place instead. -**Stewards** have the ability to veto proposals. This can happen when a proposal doesn't align with the mission/community guidelines, or when a proposal presents a significant maintenance or implementation challenge that the project is not able to tackle at that time. +**Stewards** have the ability to veto proposals. This can happen when a proposal doesn't align with the mission/community guidelines, or when a proposal presents a significant maintenance or implementation challenge that the project is not able to tackle at that time. To propose a change, open an issue. If it is a large change or a change that necessitates significant design consideration, add a 'discussion' label to the issue. Interested community members will chime in with their thoughts. After a significant period has passed (30 days unless urgent), maintainers will try to discern whether there is significant interest and whether consensus has been reached about the best approach. At this point, maintainers will either request a vote, close the issue, or open up the issue for pull requests. Pull requests submitted before a discussion has concluded will be ignored. From 7d70c3762dbc4b5b8acc694c9a0ba88ab193b5ac Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 19 Aug 2022 15:39:06 +0100 Subject: [PATCH 138/177] Add issues and pull requests guidelines for stewards --- contributor_docs/steward_guidelines.md | 143 +++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 contributor_docs/steward_guidelines.md diff --git a/contributor_docs/steward_guidelines.md b/contributor_docs/steward_guidelines.md new file mode 100644 index 0000000000..1b9ab0bdd3 --- /dev/null +++ b/contributor_docs/steward_guidelines.md @@ -0,0 +1,143 @@ +# Steward Guidelines +Whether you have just joined us as a steward, a seasoned maintainer of p5.js, or anywhere in between, this guide contains many of the information as well as tips and tricks that will help you and all the other contributors effectively contribute to p5.js. Most of what is written here are guidelines unless otherwise stated which means you can adapt the practices shown here to suit your workflow. + +# Table of Contents +- [Issues](#issues) + - [Bug report](#bug-report) + - [Feature request](#feature-request) + - [Feature enhancement](#feature-enchancement) + - [Discussion](#discussion) +- [Pull Requests](#pull-requests) + - [Simple fix](#simple-fix) + - [Bug fix](#bug-fix) + - [New feature/feature enchancement](#new-feature-feature-enchancement) + - [Dependabot](#dependabot) +- [Build Process](#build-process) +- [Release Process](#release-process) +- [Tips & Tricks](#tips-tricks) + +--- + +# Issues +We encourage most source code contributions to start with an issue and as such issues are the place where most of the discussions will take place. The steps you can take when reviewing an issue will depend on what kind of issue it is. The repo uses [Github issue templates](./.github/ISSUE_TEMPLATE) in order to better organize different kinds of issues and encourage issue author to provide all relevant information about their problem. The first step in reviewing the issue will often be looking through the filled out template and determine if you need additional information (either because some fields weren't filled in or the incorrect template was used). + +## Bug report +For bug report issues, they should be using the "Found a bug" issue template. + +1. Replicate bug + - The goal of the template is to provide enough information for a reviewer to attempt to replicate the bug in question. + - If the reported bug is not relevant to the repo it is opened in (p5.js or p5.js-website). + - Transfer the issue to the relevant repo if you have access to them. + - Otherwise leave a comment about where the bug report should be filed (with direct link provided) and close the issue. + - The first step to review a bug report is to see if enough information is provided for a bug replication and if so, attempt to replicate the bug as described. +2. If the bug can be replicated + - Some discussions may be required to determine the best way to fix a particular bug. Sometimes these may be straightforward, sometimes they can be tricky. Please refer to [p5.js' design principles](./design_principles.md) when making this decision on a case by case basis. + - If the issue author indicated in the issue they are willing to contribute a fix. + - Approve the issue for fixing by the issue author by leaving a comment and assigning them to the issue (by using the cog button on the right side next to "Assignee"). + - If the issue author does not wish to contribute a fix. + - Leave a comment recognizing the bug is replicable. + - Attempt to fix yourself or add the `help wanted` label to signal an issue needing a fix. +3. If the bug cannot be replicated + - Ask for additional info if not already provided in the template (p5.js version, browser version, OS version, etc can all be useful). + - If your testing environment differs from what is reported in the issue (different browser or OS). + - Leave a comment saying you are not able to replicate in your specific environment. + - Add a `help wanted` label to the issue and ask for someone else with the setup specified in the issue to try and replicate the bug. + - Sometimes bug can occur only when using the web editor and not when testing locally, in this case the issue should be redirected to the [web editor repo](https://github.com/processing/p5.js-web-editor). + - If a replication is possible later, go back to step 2. +4. If the bug stems from the provided example code and not p5.js' behaviour + - Determine if p5.js' documentation, code implementation, or friendly error system can be improved in order to prevent the same mistake being made. + - Kindly redirect any further questions to the [forum](https://discourse.processing.org/) and close the issue if no further changes are to be made to p5.js. + +## Feature request +For feature request issues, they should be using the "New Feature Request" issue template. + +1. As part of p5.js' commitment to increase access, all feature request must make the case for how it increases access of p5.js to communities that are historically marginalized in the field. More details are available [here](./access.md). + - If a feature request does not have the "Increasing Access" field sufficiently filled out, the issue author can be asked for how the feature increases access. + - The access statement of a feature can be provided by a different member of the community including the issue reviewer themselves. +2. The proposed new feature request can be assessed for inclusion based on the following criteria. + - Does it fit into the project scope and [design principles](./design_principles.md) of p5.js? + - A request to add a new drawing primitive shape may be considered, but a request to adopt a browser based IOT protocol will likely be out of scope. + - Overall the scope of p5.js should be relatively narrow in order to avoid excessive bloat from rarely used features. + - If a feature does not fit into the scope of p5.js, said feature can be implemented as an addon library by the issue author or a different member of the community. + - Is it likely to be considered a breaking change? + - Will it conflict with existing p5.js functions and variables? + - Will it conflict with typical sketches already written for p5.js? + - Features that are likely to break the above should be considered breaking changes and without a major version release, we should not make breaking changes to p5.js. + - Can the proposed new feature be achieved using existing functionalities already in p5.js, relatively simple native Javascript code, or existing easy to use libraries? + - Eg. instead of prividing a p5.js function to join an array of strings such as `join(["Hello", "world!"])`, the native Javascript `["Hello", "world!"].join()` should be preferred instead. +3. Provided the access requirement and other considerations have been fulfilled, at least two stewards or maintainers must approve the new feature request before work should begin towards a PR. The PR review process for new features is documented below. + +## Feature enchancement +For feature enchancement issues, they should be using the "Existing Feature Enhancement" issue template. The process here very similar to new feature request. The difference between new feature request and feature enchancement can be blurred however feature enchancement mainly deals with existing functions of p5.js while new feature request could be requesting entirely new functions to be added. + +1. Similar to new feature request, feature enchancement should only be accepted if they increases access of p5.js. Please see point 1 of [section above](#feature-request) +2. Inclusion criterias for feature enchancement are similar to those for feature request above but particular attention should be paid to potential breaking changes. + - If modifying existing functions, all previous valid and documented function signatures must behave in the same way. +3. Feature enchancements must be approved by at least one steward or maintainer before work should begin towards a PR. The PR review process for feature enchancement is documented below. + +## Discussion +This type of issue has a minimal template ("Discussion") and should be use only if a particular discussion doesn't fall under the other three existing templates or be better suited to the forum or Discord. + +- If an issue is opened as a discussion but should be, for example, a bug report, the correct labeled should be applied and the "discussion" label removed. Additional info about the bug should also be requested from the author if not already included (following [Bug report](#bug-report)) above. +- If an issue is opened as a discussion but isn't relevant to source code contribution or otherwise relevant to the Github repositories/contribution process/contribution community (eg. a discussion about the best kind of projector to use for an exhibition showing sketches done with p5.js), they should be redirected to the forum or Discord and the issue closed. +- If relevant, additional labels should be added to discussion issues to further signal what type of discussion it is at a glance. + +--- + +# Pull Requests +Almost all code contribution to the p5.js repositories happens through pull requests, stewards and maintainers may have push access to the repositories but are still encouraged to follow the same issue > PR > review process when contributing code. Following are some steps that can be taken when reviewing a PR. + +- Pull request template can be found [here](../.github/PULL_REQUEST_TEMPLATE.md). +- Almost all pull requests must have associated issues opened and discussed first, meaning the relevant [issue workflow](#issues) must have been followed first before a PR should be reviewed by any steward or maintainer. + - The only instance where this does not apply are very minor typo fixes, which does not require an opened issue and can be merged by anyone with merge access to the repo, even if they are not stewards of a particular area. + - While this exception exist, we will apply it in practice only but contributors are usually encouraged to open new issues. In other words, if in doubt about whether this exception applies, just open an issue anyway. +- If a pull request does not fully solve the referenced issue, you can edit the original post and change "Resovles #OOOO" to "Addresses #OOOO" so that it does not automatically close the original issue when this PR is merged. + +## Simple fix +Simple fix such as small typo fix can be merged directly by anyone with merge access with a quick check on the PR "Files Changed" tab and that automated CI test passes. + +## Bug fix +1. Bug fixes should be reviewed by the relevant area steward, ideally the same one that approved the referenced issue for fixing. +2. The PR "Files Changed" tab can be used to initially review whether the fix is as described in the discussion in the issue. +3. The PR should be tested locally whenever possible and relevant. The Github CLI can be helpful in streamlining some of the process. (See more below in [Tips & Tricks](#tips-tricks)). + - The fix should address the original issue sufficiently + - The fix should not change any existing behaviours unless agreed upon in the original issue + - The fix should not have significant performance impact on p5.js + - The fix should not have any impact on p5.js' accessibility + - The fix should use modern standard of Javascript coding + - The fix should pass all automated tests and include new tests if relevant +4. If any additional changes are required, line comments should be added to the relevant lines as described [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#adding-line-comments-to-a-pull-request) + - A suggestion block can also be used to suggest specific changes + - If there are multiple changes that are required, instead of adding single line comments many times, follow the procedure documented [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/reviewing-proposed-changes-in-a-pull-request) to make multiple line comments and single request for changes + - If line comments are just for clarification or discussion and not changes are requested yet, in the previous step instead of choosing "Request changes", choose "Comment" instead. +5. Once the PR has been reviewed and no additional changes are required, a steward and mark the PR as "Approved" by choosing the "Approve" option in the previous step (instead of "Comment" or "Request changes") with or without additional comments. The steward can then either request additional review by another steward or maintainer if desired, or merge the PR if they have merge access or a maintainer will merge the approved PR. +6. @all-contributors bot should be called to add any new contributors to the list of contributors in the README.md file. +``` +@all-contributors please add @[github handle] for [contribution type] +``` + +## New feature/feature enchancement +The process for new feature or feature enhancement PR is similar to bug fixes with just one notable difference. + +- A new feature/feature enchancement PR must be reviewed and approved by at least two stewards or maintainer before it can be merged. + - This can be the same two stewards or maintainer that approved the original issue or not. + +## Dependabot +Dependabot PRs are usually only visible to repo admins so if this does not apply to you, please skip this section. + +- Dependabot PR can be merged directly if the version update is a semver patch version as long as automated CI test passes. +- Dependabot PR with semver minor version changes can usually be merged directly as long as automated CI test passes, but a quick check on the changelog of the updated dependency is recommended. +- Dependabot PR with semver major version changes may likely affect either the build process or p5.js functionalities. The reviewer in this case is encouraged to review the changelog from current version to target version if possible and test the PR locally to ensure all processes are functioning and make any required changes due to potential breaking changes in the dependencies. + - Many dependencies bump major version number only because they drop official support for very old versions of node.js, which means in many cases, major version change don't necessary mean breaking changes resulting from dependency API changes. + +--- + +# Build process + +--- + +# Release process + +--- + +# Tips & tricks \ No newline at end of file From ebd2d6146bd80b44897733798a8347157468afc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sat, 20 Aug 2022 13:22:06 +0200 Subject: [PATCH 139/177] parsing arguments and adding options object --- src/image/loading_displaying.js | 72 ++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 48d991aba1..077611c4d7 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -163,16 +163,26 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * Generates a gif of your current animation and downloads it to your computer! * * The duration argument specifies how many seconds you want to record from your animation. - * This value is then converted to the necessary number of frames to generate it. + * This value is then converted to the necessary number of frames to generate it, depending + * on the value of units. More on that on the next paragraph. * - * With the delay argument, you can tell the function to skip the first `delay` seconds - * of the animation, and then download the `duration` next seconds. This means that regardless - * of the value of `delay`, your gif will always be `duration` seconds long. + * An optional object that can contain two more arguments: delay (number) and units (string). + * + * `delay`, specifying how much time we should wait before recording + * + * `units`, a string that can be either 'seconds' or 'frames'. By default it's 'seconds'. + * + * `units` specifies how the duration and delay arguments will behave. + * If 'seconds', these arguments will correspond to seconds, meaning that 3 seconds worth of animation + * will be created. If 'frames', the arguments now correspond to the number of frames you want your + * animation to be, if you are very sure of this number. * * @method saveGif * @param {String} filename File name of your gif * @param {Number} duration Duration in seconds that you wish to capture from your sketch - * @param {Number} delay Duration in seconds that you wish to wait before starting to capture + * @param {Object} options An optional object that can contain two more arguments: delay, specifying + * how much time we should wait before recording, and units, a string that can be either 'seconds' or + * 'frames'. By default it's 'seconds'. * * @example *
@@ -207,18 +217,47 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * @alt * animation of a circle moving smoothly diagonally */ -p5.prototype.saveGif = async function(fileName, seconds, delay) { - // validate parameters +p5.prototype.saveGif = async function( + fileName, + duration, + { delay = 0, units = 'seconds' } +) { + // validate parameters to throw friendly error p5._validateParameters('saveGif', arguments); + // throwing exception for tests + let delayOption = arguments[2].delay; + let unitsOption = arguments[2].units; + + if (typeof fileName !== String) + throw TypeError('saveGif(): First argument should be a string'); + if (typeof duration !== Number) + throw TypeError('saveGif(): Second argument should be a number'); + if (typeof arguments[2] !== Object) + throw TypeError('saveGif(): Third argument should be an object'); + if (typeof delayOption !== Number) { + throw TypeError( + 'saveGif() options: first option "delay" should be a number' + ); + } + if (unitsOption !== 'seconds' || unitsOption !== 'frames') { + throw TypeError( + 'saveGif() options: second option "units" should either be "seconds" or "frames"' + ); + } + // get the project's framerate - // if it is undefined or some non useful value, assume it's 60 let _frameRate = this._targetFrameRate; + // if it is undefined or some non useful value, assume it's 60 if (_frameRate === Infinity || _frameRate === undefined || _frameRate === 0) { _frameRate = 60; } - // calculate delay based on frameRate + // calculate frame delay based on frameRate + + // this delay has nothing to do with the + // delay in options, but rather is the delay + // we have to specify to the gif encoder between frames. let gifFrameDelay = 1 / _frameRate * 1000; // constrain it to be always greater than 20, @@ -226,15 +265,14 @@ p5.prototype.saveGif = async function(fileName, seconds, delay) { // reference: https://stackoverflow.com/questions/64473278/gif-frame-duration-seems-slower-than-expected gifFrameDelay = gifFrameDelay < 20 ? 20 : gifFrameDelay; - // because the input was in seconds, we now calculate - // how many frames those seconds translate to - let nFrames = Math.ceil(seconds * _frameRate); - let nFramesDelay = Math.ceil(delay * _frameRate); + // check the mode we are in and how many frames + // that duration translates to + const nFrames = units === 'seconds' ? duration * _frameRate : duration; + const nFramesDelay = units === 'seconds' ? delay * _frameRate : delay; - // initialize variables for the frames processing + // initialize variables for the frames processing let count = nFramesDelay; - this.noLoop(); // we start on the frame set by the delay argument frameCount = nFramesDelay; @@ -266,6 +304,9 @@ p5.prototype.saveGif = async function(fileName, seconds, delay) { pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); } + // stop the loop since we are going to manually redraw + this.noLoop(); + while (count < nFrames + nFramesDelay) { /* we draw the next frame. this is important, since @@ -402,6 +443,7 @@ p5.prototype.saveGif = async function(fileName, seconds, delay) { // Get a direct typed array view into the buffer to avoid copying it const buffer = gif.bytesView(); const extension = 'gif'; + const blob = new Blob([buffer], { type: 'image/gif' }); From 133eafa037790aac193af84866c3669f7d1dc64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 21 Aug 2022 10:03:39 +0200 Subject: [PATCH 140/177] remove unnecessary code and update example --- package-lock.json | 3 ++- src/image/loading_displaying.js | 25 ++----------------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe4acfa173..1a0835fbe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6830,7 +6830,8 @@ "grunt-babel": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/grunt-babel/-/grunt-babel-8.0.0.tgz", - "integrity": "sha512-WuiZFvGzcyzlEoPIcY1snI234ydDWeWWV5bpnB7PZsOLHcDsxWKnrR1rMWEUsbdVPPjvIirwFNsuo4CbJmsdFQ==" + "integrity": "sha512-WuiZFvGzcyzlEoPIcY1snI234ydDWeWWV5bpnB7PZsOLHcDsxWKnrR1rMWEUsbdVPPjvIirwFNsuo4CbJmsdFQ==", + "dev": true }, "grunt-cli": { "version": "1.3.2", diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 077611c4d7..8d0a71e8c3 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -209,7 +209,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * // or keyPressed for example * function mousePressed() { * // this will download the first two seconds of my animation! - * saveGif('mySketch', 2); + * saveGif('mySketch', 2, {units: 'seconds', delay: 0}); * } * *
@@ -225,27 +225,6 @@ p5.prototype.saveGif = async function( // validate parameters to throw friendly error p5._validateParameters('saveGif', arguments); - // throwing exception for tests - let delayOption = arguments[2].delay; - let unitsOption = arguments[2].units; - - if (typeof fileName !== String) - throw TypeError('saveGif(): First argument should be a string'); - if (typeof duration !== Number) - throw TypeError('saveGif(): Second argument should be a number'); - if (typeof arguments[2] !== Object) - throw TypeError('saveGif(): Third argument should be an object'); - if (typeof delayOption !== Number) { - throw TypeError( - 'saveGif() options: first option "delay" should be a number' - ); - } - if (unitsOption !== 'seconds' || unitsOption !== 'frames') { - throw TypeError( - 'saveGif() options: second option "units" should either be "seconds" or "frames"' - ); - } - // get the project's framerate let _frameRate = this._targetFrameRate; // if it is undefined or some non useful value, assume it's 60 @@ -274,7 +253,7 @@ p5.prototype.saveGif = async function( let count = nFramesDelay; // we start on the frame set by the delay argument - frameCount = nFramesDelay; + // frameCount = nFramesDelay; const lastPixelDensity = this._pixelDensity; this.pixelDensity(1); From f42fef88112c3fb422409cd3ec6a8a5b355c99ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 21 Aug 2022 10:04:10 +0200 Subject: [PATCH 141/177] fix lint issue in example --- src/image/loading_displaying.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 8d0a71e8c3..5995ec5f64 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -209,7 +209,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * // or keyPressed for example * function mousePressed() { * // this will download the first two seconds of my animation! - * saveGif('mySketch', 2, {units: 'seconds', delay: 0}); + * saveGif('mySketch', 2, { units: 'seconds', delay: 0 }); * } *
*
From 7daec8ed88c2735c6155c6aa9000c874ed4d8795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 21 Aug 2022 11:07:52 +0200 Subject: [PATCH 142/177] added more tests! --- test/unit/image/downloading.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/test/unit/image/downloading.js b/test/unit/image/downloading.js index 67419dfe02..fa3df09dd1 100644 --- a/test/unit/image/downloading.js +++ b/test/unit/image/downloading.js @@ -340,9 +340,36 @@ suite('p5.prototype.saveGif', function() { assert.ok(myp5.saveGif); assert.typeOf(myp5.saveGif, 'function'); }); + test('should not throw an error', function() { - myp5.saveGif('myGif', 3, 2); + myp5.saveGif('myGif', 3); + }); + + test('should not throw an error', function() { + myp5.saveGif('myGif', 3, { delay: 2, frames: 'seconds' }); + }); + + test('wrong parameter type #0', function(done) { + assert.validationError(function() { + myp5.saveGif(2, 2); + done(); + }); + }); + + test('wrong parameter type #1', function(done) { + assert.validationError(function() { + myp5.saveGif('mySketch', '2'); + done(); + }); + }); + + test('wrong parameter type #2', function(done) { + assert.validationError(function() { + myp5.saveGif('mySketch', 2, 'delay'); + done(); + }); }); + testWithDownload('should download a GIF', async function(blobContainer) { myp5.saveGif(myGif, 3, 2); await waitForBlob(blobContainer); From c8baf6e3f036ecf89ed6879d608fe91a7808d6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Sun, 21 Aug 2022 11:08:12 +0200 Subject: [PATCH 143/177] addressing mentor's feedback --- src/image/loading_displaying.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 5995ec5f64..8fb0a891cc 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -248,12 +248,10 @@ p5.prototype.saveGif = async function( // that duration translates to const nFrames = units === 'seconds' ? duration * _frameRate : duration; const nFramesDelay = units === 'seconds' ? delay * _frameRate : delay; + const totalNumberOfFrames = nFrames + nFramesDelay; // initialize variables for the frames processing - let count = nFramesDelay; - - // we start on the frame set by the delay argument - // frameCount = nFramesDelay; + let frameIterator = nFramesDelay; const lastPixelDensity = this._pixelDensity; this.pixelDensity(1); @@ -261,8 +259,9 @@ p5.prototype.saveGif = async function( // We first take every frame that we are going to use for the animation let frames = []; - if (document.getElementById('progressBar') !== null) - document.getElementById('progressBar').remove(); + let progressBarIdName = 'p5.gif.progressBar'; + if (document.getElementById(progressBarIdName) !== null) + document.getElementById(progressBarIdName).remove(); let p = this.createP(''); p.id('progressBar'); @@ -286,13 +285,13 @@ p5.prototype.saveGif = async function( // stop the loop since we are going to manually redraw this.noLoop(); - while (count < nFrames + nFramesDelay) { + while (frameIterator < totalNumberOfFrames) { /* we draw the next frame. this is important, since busy sketches or low end devices might take longer to render some frames. So we just wait for the frame to be drawn and immediately save it to a buffer and continue - */ + */ this.redraw(); // depending on the context we'll extract the pixels one way @@ -320,7 +319,7 @@ p5.prototype.saveGif = async function( } frames.push(data); - count++; + frameIterator++; p.html( 'Saved frame ' + From d9c5fe0a022a130c5742ca68d4ed5aa23b2ebc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 22 Aug 2022 21:51:09 +0200 Subject: [PATCH 144/177] sanity and type checking for the arguments in options object --- src/image/loading_displaying.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 8fb0a891cc..06a66bf1b7 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -225,6 +225,17 @@ p5.prototype.saveGif = async function( // validate parameters to throw friendly error p5._validateParameters('saveGif', arguments); + let optionsArg = arguments[arguments.length - 1]; + + // if arguments in the options object are not correct, cancel operation + if (typeof optionsArg.delay !== 'number') { + console.log(optionsArg.delay, typeof optionsArg.delay); + throw TypeError('Delay parameter must be a number'); + } + if (optionsArg.units !== 'seconds' || optionsArg.units !== 'frames') { + throw TypeError('Units parameter must be either "frames" or "seconds"'); + } + // get the project's framerate let _frameRate = this._targetFrameRate; // if it is undefined or some non useful value, assume it's 60 From 7be6a621c42290e97998bd52ff2d16c1dd7e364f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 22 Aug 2022 22:54:31 +0200 Subject: [PATCH 145/177] better validation of arguments --- src/image/loading_displaying.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 06a66bf1b7..73c1141f57 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -220,22 +220,30 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { p5.prototype.saveGif = async function( fileName, duration, - { delay = 0, units = 'seconds' } + options = { delay: 0, units: 'seconds' } ) { - // validate parameters to throw friendly error - p5._validateParameters('saveGif', arguments); - - let optionsArg = arguments[arguments.length - 1]; - + // validate parameters + if (typeof fileName !== 'string') { + throw TypeError('fileName parameter must be a string'); + } + if (typeof duration !== 'number') { + throw TypeError('Duration parameter must be a number'); + } // if arguments in the options object are not correct, cancel operation - if (typeof optionsArg.delay !== 'number') { - console.log(optionsArg.delay, typeof optionsArg.delay); + if (typeof options.delay !== 'number') { throw TypeError('Delay parameter must be a number'); } - if (optionsArg.units !== 'seconds' || optionsArg.units !== 'frames') { + // if units is not seconds nor frames, throw error + if (options.units !== 'seconds' && options.units !== 'frames') { throw TypeError('Units parameter must be either "frames" or "seconds"'); } + // extract variables for more comfortable use + let units = options.units; + let delay = options.delay; + + // console.log(options); + // get the project's framerate let _frameRate = this._targetFrameRate; // if it is undefined or some non useful value, assume it's 60 From 928ee37d3f365196b06aa3d70167f799b3592cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 22 Aug 2022 23:52:15 +0200 Subject: [PATCH 146/177] improve example and add frameCount support --- src/image/loading_displaying.js | 56 ++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 73c1141f57..c2beed7135 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -167,49 +167,54 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { * on the value of units. More on that on the next paragraph. * * An optional object that can contain two more arguments: delay (number) and units (string). - * + * * `delay`, specifying how much time we should wait before recording - * - * `units`, a string that can be either 'seconds' or 'frames'. By default it's 'seconds'. - * + * + * `units`, a string that can be either 'seconds' or 'frames'. By default it's 'seconds'. + * * `units` specifies how the duration and delay arguments will behave. - * If 'seconds', these arguments will correspond to seconds, meaning that 3 seconds worth of animation + * If 'seconds', these arguments will correspond to seconds, meaning that 3 seconds worth of animation * will be created. If 'frames', the arguments now correspond to the number of frames you want your * animation to be, if you are very sure of this number. * * @method saveGif * @param {String} filename File name of your gif * @param {Number} duration Duration in seconds that you wish to capture from your sketch - * @param {Object} options An optional object that can contain two more arguments: delay, specifying - * how much time we should wait before recording, and units, a string that can be either 'seconds' or - * 'frames'. By default it's 'seconds'. + * @param {Object} options An optional object that can contain two more arguments: delay, specifying + * how much time we should wait before recording, and units, a string that can be either 'seconds' or + * 'frames'. By default it's 'seconds'. * * @example *
* * function setup() { - * createCanvas(100, 100); - * colorMode(HSL); + * createCanvas(100,100); * } - + * * function draw() { - * // create some cool dynamic background - * let hue = map(sin(frameCount / 100), -1, 1, 0, 100); - * background(hue, 40, 60); - - * // create a circle that moves diagonally - * circle( - * 100 * sin(frameCount / 10) + width / 2, - * 100 * sin(frameCount / 10) + height / 2, - * 10 - * ); + * colorMode(RGB); + * background(30); + * + * // create a bunch of circles that move in... circles! + * for (let i = 0; i < 10; i++) { + * let opacity = map(i, 0, 10, 0, 255); + * noStroke(); + * fill(230, 250, 90, opacity); + * circle( + * 30 * sin(frameCount / (30 - i)) + width / 2, + * 30 * cos(frameCount / (30 - i)) + height / 2, + * 10 + * ); + * } * } - + * * // you can put it in the mousePressed function, * // or keyPressed for example - * function mousePressed() { - * // this will download the first two seconds of my animation! - * saveGif('mySketch', 2, { units: 'seconds', delay: 0 }); + * function keyPressed() { + * // this will download the first 5 seconds of the animation! + * if (key === 's') { + * saveGif('mySketch', 5); + * } * } * *
@@ -271,6 +276,7 @@ p5.prototype.saveGif = async function( // initialize variables for the frames processing let frameIterator = nFramesDelay; + frameCount = frameIterator; const lastPixelDensity = this._pixelDensity; this.pixelDensity(1); From 64c4d7afc1dea3ceb35997e5e10851c03713222b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Mon, 22 Aug 2022 23:52:32 +0200 Subject: [PATCH 147/177] fix linter error in example --- src/image/loading_displaying.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index c2beed7135..ea4b23af0c 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -188,7 +188,7 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { *
* * function setup() { - * createCanvas(100,100); + * createCanvas(100, 100); * } * * function draw() { From 8931fb1703ed6493567967c1215e0e0afff23391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Enrique=20Rasc=C3=B3n?= Date: Tue, 23 Aug 2022 00:06:46 +0200 Subject: [PATCH 148/177] put sketch back to normal --- lib/empty-example/sketch.js | 265 +----------------------------------- 1 file changed, 2 insertions(+), 263 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index d9e79ed6b5..69b202ee71 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,268 +1,7 @@ -/* eslint-disable no-unused-vars */ - -// let saving = false; -// function setup() { -// // put setup code here -// createCanvas(500, 500); -// } - -// function draw() { -// // put drawing code here -// let hue = map(sin(frameCount), -1, 1, 127, 255); -// let hue_2 = map(sin(frameCount / 100) + 0.791, -1, 1, 127, 255); - -// strokeWeight(0); -// line(width / 2, 0, width / 2, height); -// line(0, height / 2, width, height / 2); - -// fill(250, 250, 20); -// rect(0, 0, width / 2, height / 2); - -// // fill(80, 80, hue); -// rect(width / 2, 0, width / 2, height / 2); - -// fill(20, 250, 250); -// rect(0, height / 2, width / 2, height / 2); - -// // fill(240, 240, 0); -// rect(width / 2, height / 2, width / 2, height / 2); - -// fill(30); -// stroke(20, 250, 20); -// strokeWeight(4); -// circle( -// 100 * sin(frameCount / 20) + width / 2, -// // 100 * sin(frameCount / 20) + height / 2, -// // width / 2, -// height / 2, -// 100 -// ); - -// if (saving) { -// save('frame' + frameCount.toString()); -// } -// } - -// function mousePressed() { -// if (mouseButton === RIGHT) { -// saveGif('mySketch', 1, 3); -// } -// } - -// function keyPressed() { -// switch (key) { -// case 's': -// frameRate(3); -// frameCount = 0; -// saving = !saving; - -// if (!saving) frameRate(60); -// break; -// } -// } - -// / COMPLEX SKETCH -let offset; -let spacing; - function setup() { - // randomSeed(1312); - - w = min(windowHeight, windowWidth); - createCanvas(w, w); - print(w); - looping = false; - saving = false; - noLoop(); - - divisor = random(1.2, 3).toFixed(2); - - frameWidth = w / divisor; - offset = (-frameWidth + w) / 2; - - gen_num_total_squares = int(random(2, 20)); - spacing = frameWidth / gen_num_total_squares; - - initHue = random(0, 360); - compColor = (initHue + 360 / random(1, 4)) % 360; - - gen_stroke_weight = random(-100, 100); - gen_stroke_fade_speed = random(30, 150); - gen_shift_small_squares = random(0, 10); - - gen_offset_small_sq_i = random(3, 10); - gen_offset_small_sq_j = random(3, 10); - - gen_rotation_speed = random(30, 250); - - gen_depth = random(5, 20); - gen_offset_i = random(1, 10); - gen_offset_j = random(1, 10); - - gen_transparency = random(20, 255); - - background(24); - // saveGif('mySketch', 2); + // put setup code here } function draw() { - colorMode(HSB); - background(initHue, 80, 20, gen_transparency); - makeSquares(); - // addHandle(); - - if (saving) save('grid' + frameCount + '.png'); -} - -function makeSquares(depth = gen_depth) { - colorMode(HSB); - let count_i = 0; - - for (let i = offset; i < w - offset; i += spacing) { - let count_j = 0; - count_i++; - - if (count_i > gen_num_total_squares) break; - - for (let j = offset; j < w - offset; j += spacing) { - count_j++; - - if (count_j > gen_num_total_squares) break; - - for (let n = 0; n < depth; n++) { - noFill(); - - if (n === 0) { - stroke(initHue, 100, 100); - fill( - initHue, - 100, - 100, - map( - sin( - gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed - ), - -1, - 1, - 0, - 0.3 - ) - ); - } else { - stroke(compColor, map(n, 0, depth, 100, 0), 100); - fill( - compColor, - 100, - 100, - map( - cos( - gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed - ), - -1, - 1, - 0, - 0.3 - ) - ); - } - - strokeWeight( - map( - sin( - gen_stroke_weight * (i + j) + frameCount / gen_stroke_fade_speed - ), - -1, - 1, - 0, - 1.5 - ) - ); - - push(); - translate(i + spacing / 2, j + spacing / 2); - - rotate( - i * gen_offset_i + - j * gen_offset_j + - frameCount / (gen_rotation_speed / (n + 1)) - ); - - if (n % 2 !== 0) { - translate( - sin(frameCount / 50) * gen_shift_small_squares, - cos(frameCount / 50) * gen_shift_small_squares - ); - rotate(i * gen_offset_i + j * gen_offset_j + frameCount / 100); - } - - if (n > 0) - rect( - -spacing / (gen_offset_small_sq_i + n), - -spacing / (gen_offset_small_sq_j + n), - spacing / (n + 1), - spacing / (n + 1) - ); - else rect(-spacing / 2, -spacing / 2, spacing, spacing); - - pop(); - } - // strokeWeight(40); - // point(i, j); - } - } -} - -function addHandle() { - fill(40); - noStroke(); - textAlign(RIGHT, BOTTOM); - textFont(font); - textSize(20); - text('@jesi_rgb', w - 30, w - 30); -} - -function mousePressed() { - if (mouseButton === LEFT) { - if (looping) { - noLoop(); - looping = false; - } else { - loop(); - looping = true; - } - } -} - -function keyPressed() { - console.log(key); - switch (key) { - // pressing the 's' key - case 's': - saveGif('mySketch', 2); - break; - - // pressing the '0' key - case '0': - frameCount = 0; - loop(); - noLoop(); - break; - - // pressing the ← key - case 'ArrowLeft': - frameCount >= 0 ? (frameCount -= 1) : (frameCount = 0); - noLoop(); - console.log(frameCount); - break; - - // pressing the → key - case 'ArrowRights': - frameCount += 1; - noLoop(); - console.log(frameCount); - break; - - default: - break; - } + // put drawing code here } From b273267f1e8c74f78ad4b2b106b495be0d6cf2af Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 24 Aug 2022 15:31:02 +0100 Subject: [PATCH 149/177] Start contributor guidelines document --- contributor_docs/README.md | 6 +++++ contributor_docs/contributor_guidelines.md | 26 ++++++++++++++++++++++ contributor_docs/steward_guidelines.md | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 contributor_docs/contributor_guidelines.md diff --git a/contributor_docs/README.md b/contributor_docs/README.md index 70a9c26453..efb24968c4 100644 --- a/contributor_docs/README.md +++ b/contributor_docs/README.md @@ -34,6 +34,12 @@ For a typical contribution to the p5.js or p5.js-website repository, we will fol 6. Discuss 7. Approved and merged +Head over to [this link](./contributor_guidelines.md) where you will be guided one step at a time on how to navigate the steps above, or you can also use the table of contents on the same page to skip to a relevant part you need a refresher on. + +Most of the time we will stick with this workflow quite strictly and, especially if you have contributed to other projects before, it may feel like there are too many hoops to jump through for what may be a simple contribution. However, the steps above are aimed to make it easy for you as a contributor and for stewards/maintainers to contribute meaningfully, while also making sure that you won't be spending time working on things that may not be accepted for various reasons. The steps above will help ensure that any proposals or fixes are adequately discussed and considered before any work begin, and often this will actually save you (and the steward/maintainer) time because the PR that would need additional fixing after review, or outright not accepted, would happen less often as a result. + +We see contributing to p5.js as a learning opportunity and we don't measure sucess by only looking at the volume of contributions we received. There is no time limit on how long it takes you to complete a contribution, so take your time and work at your own pace. Ask for help from any of the stewards or maintainers if you need them and we'll try our best to support you. + ## Non-source code contribution There are many more ways to contribute to p5.js through non-source code contribution than can be exhaustively list here, some of the ways may also involve working with some of the p5.js repositories (such as adding example, writing tutorial for the website, etc). Depending on what the planned contribution is, we may be able to support you in different ways so do reach out to us via any channel available to you (email, social media, Discourse forum, Discord, etc). diff --git a/contributor_docs/contributor_guidelines.md b/contributor_docs/contributor_guidelines.md new file mode 100644 index 0000000000..4a17855ca6 --- /dev/null +++ b/contributor_docs/contributor_guidelines.md @@ -0,0 +1,26 @@ +# Contributor Guidelines +Welcome to the contributor guidelines! This document is for new contributors looking to contribute code to p5.js, contributors looking to refresh their memories on some technical steps, or just about anything else to do with code contributions to p5.js. + +If you are looking to contribute outside of the p5.js repositories (writing tutorials, planning classes, organizing events), please have a look at the other relevant pages instead. Stewards or maintainers may find the [steward guidelines](./steward_guidelines.md) more helpful regarding reviewing issues and pull requests. + +This is a fairly long and comprehensive document but we will try to deliniate all steps and points as clearly as possible. Do utilize the table of contents, the browser search functionality (`Ctrl + f` or `Cmd + f`) to find sections relevant to you. Feel free to skip sections if they are not relevant to your planned contributions as well. + +# Table of Contents +- All about issues +- Pull requests + +--- +# All about issues +The majority of the activity on p5.js' Github repositories (repo for short) happens in issues and issues will most likely be the place to start your contribution process as well. + +## What are issues? +Issue is the generic name for a post on Github that aims to describe, well, an issue. This "issue" can be a bug report, a request to add new feature, a discussion, a question, an announcement, or anything that works as a post. Comments can be added below each issue by anyone with a Github account, including bots! It is the place where contributors dicusses topics related to the development of the project in the repo. + +While an issue can be opened for a wide variety of reasons, for p5.js' repos we usually only use issues to discuss p5.js source code development related topics. Topics such as debugging your own code, inviting collaborators to your project, or other unrelated topics should be discuss either on the [forum](https://discourse.processing.com) or on other platforms. + +We have created easy to use issue templates to aid you in deciding whether a topic should be a Github issue or it should be posted somewhere else! + +## Issue templates + +--- +# Pull requests \ No newline at end of file diff --git a/contributor_docs/steward_guidelines.md b/contributor_docs/steward_guidelines.md index 1b9ab0bdd3..d881323204 100644 --- a/contributor_docs/steward_guidelines.md +++ b/contributor_docs/steward_guidelines.md @@ -14,7 +14,7 @@ Whether you have just joined us as a steward, a seasoned maintainer of p5.js, or - [Dependabot](#dependabot) - [Build Process](#build-process) - [Release Process](#release-process) -- [Tips & Tricks](#tips-tricks) +- [Tips & Tricks](#tips--tricks) --- From 9bf65268fae4a825bf180631facea7f88eeca82f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 25 Aug 2022 18:57:07 -0400 Subject: [PATCH 150/177] Add support for QUAD and QUAD_STRIP in WebGL mode --- src/webgl/p5.RendererGL.Immediate.js | 65 ++++++++++++++- .../webgl/geometryQuads/index.html | 25 ++++++ .../webgl/geometryQuads/sketch.js | 48 +++++++++++ test/unit/webgl/p5.RendererGL.js | 80 +++++++++++++++++++ 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 test/manual-test-examples/webgl/geometryQuads/index.html create mode 100644 test/manual-test-examples/webgl/geometryQuads/sketch.js diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 3c23b0c56e..42642eeddf 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -26,7 +26,8 @@ import './p5.RenderBuffer'; * @param {Number} mode webgl primitives mode. beginShape supports the * following modes: * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) + * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS,, QUAD_STRIP, + * and TESS(WEBGL only) * @chainable */ p5.RendererGL.prototype.beginShape = function(mode) { @@ -36,6 +37,13 @@ p5.RendererGL.prototype.beginShape = function(mode) { return this; }; +const immediateBufferStrides = { + vertices: 1, + vertexNormals: 1, + vertexColors: 4, + uvs: 2 +}; + /** * adds a vertex to be drawn in a custom Shape. * @private @@ -47,6 +55,29 @@ p5.RendererGL.prototype.beginShape = function(mode) { * @TODO implement handling of p5.Vector args */ p5.RendererGL.prototype.vertex = function(x, y) { + // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn + // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra + // work to convert QUAD_STRIP here, since the only difference is in how edges + // are rendered.) + if (this.immediateMode.shapeMode === constants.QUADS) { + // A finished quad turned into triangles should leave 6 vertices in the + // buffer: + // 0--2 0--2 3 + // | | --> | / / | + // 1--3 1 4--5 + // When vertex index 3 is being added, add the necessary duplicates. + if (this.immediateMode.geometry.vertices.length % 6 === 3) { + for (const key in immediateBufferStrides) { + const stride = immediateBufferStrides[key]; + const buffer = this.immediateMode.geometry[key]; + buffer.push( + ...buffer.slice(buffer.length - stride, buffer.length), + ...buffer.slice(buffer.length - 2 * stride, buffer.length - stride) + ); + } + } + } + let z, u, v; // default to (x, y) mode: all other arguments assumed to be 0. @@ -233,6 +264,29 @@ p5.RendererGL.prototype._calculateEdges = function( res.push([i, i + 1]); } break; + case constants.QUADS: + // Quads have been broken up into two triangles by `vertex()`: + // 0---2 3 + // | / / | + // 1 4---5 + for (i = 0; i < verts.length - 5; i += 6) { + res.push([i, i + 1]); + res.push([i, i + 2]); + res.push([i + 4, i + 5]); + res.push([i + 3, i + 5]); + } + break; + case constants.QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + for (i = 0; i < verts.length - 2; i += 2) { + res.push([i, i + 1]); + res.push([i, i + 2]); + res.push([i + 1, i + 3]); + } + res.push([i, i + 1]); + break; default: for (i = 0; i < verts.length - 1; i++) { res.push([i, i + 1]); @@ -289,6 +343,15 @@ p5.RendererGL.prototype._drawImmediateFill = function() { this.immediateMode.shapeMode = constants.TRIANGLE_FAN; } + // WebGL 1 doesn't support the QUADS and QUAD_STRIP modes, so we + // need to convert them to a supported format. In `vertex()`, we reformat + // the input data into the formats specified below. + if (this.immediateMode.shapeMode === constants.QUADS) { + this.immediateMode.shapeMode = constants.TRIANGLES; + } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { + this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; + } + this._applyColorBlend(this.curFillColor); gl.drawArrays( this.immediateMode.shapeMode, diff --git a/test/manual-test-examples/webgl/geometryQuads/index.html b/test/manual-test-examples/webgl/geometryQuads/index.html new file mode 100644 index 0000000000..789e801b2b --- /dev/null +++ b/test/manual-test-examples/webgl/geometryQuads/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + +
+ Left: A solid rectangle broken up into triangles
+ TESTS: TRIANGLE_STRIP

+ Center: Three rectangles
+ TESTS: QUADS

+ Right: A solid rectangle broken up into quads
+ TESTS: QUAD_STRIP +
+ + + diff --git a/test/manual-test-examples/webgl/geometryQuads/sketch.js b/test/manual-test-examples/webgl/geometryQuads/sketch.js new file mode 100644 index 0000000000..73c398979a --- /dev/null +++ b/test/manual-test-examples/webgl/geometryQuads/sketch.js @@ -0,0 +1,48 @@ +let angle, px, py; +let img; +const sz = 25; + +function preload() { + img = loadImage('../assets/UV_Grid_Sm.jpg'); +} + +function setup() { + createCanvas(600, 600, WEBGL); + setAttributes('antialias', true); + fill(63, 81, 181); + strokeWeight(2); + noLoop(); +} + +function draw() { + background(250); + + // Reference: TRIANGLE_STRIP + push(); + translate(-width / 4, -250); + drawQuads(TRIANGLE_STRIP); + pop(); + + // Test 1: QUADS + push(); + translate(0, -250); + drawQuads(QUADS); + pop(); + + // Test 2: QUAD_STRIP + push(); + translate(width / 4, -250); + drawQuads(QUAD_STRIP); + pop(); +} + +function drawQuads(mode) { + beginShape(mode); + for (let y = 0; y <= 500; y += 100) { + for (const side of [-1, 1]) { + fill(random(255), random(255), random(255)); + vertex(side * 40, y); + } + } + endShape(); +} diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 2195bbe843..64c548d714 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -522,4 +522,84 @@ suite('p5.RendererGL', function() { }); }); }); + + suite('beginShape() in WEBGL mode', function() { + test('QUADS mode converts into triangles', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + renderer.beginShape(myp5.QUADS); + renderer.vertex(0, 0); + renderer.vertex(0, 1); + renderer.vertex(1, 0); + renderer.vertex(1, 1); + + renderer.vertex(2, 0); + renderer.vertex(2, 1); + renderer.vertex(3, 0); + renderer.vertex(3, 1); + renderer.endShape(); + + const expected = [ + [0, 0], + [0, 1], + [1, 0], + + [1, 0], + [0, 1], + [1, 1], + + [2, 0], + [2, 1], + [3, 0], + + [3, 0], + [2, 1], + [3, 1] + ]; + assert.equal( + renderer.immediateMode.geometry.vertices.length, + expected.length + ); + expected.forEach(function([x, y], i) { + assert.equal(renderer.immediateMode.geometry.vertices[i].x, x); + assert.equal(renderer.immediateMode.geometry.vertices[i].y, y); + }); + done(); + }); + + test('QUADS mode makes edges for quad outlines', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + renderer.beginShape(myp5.QUADS); + renderer.vertex(0, 0); + renderer.vertex(0, 1); + renderer.vertex(1, 0); + renderer.vertex(1, 1); + + renderer.vertex(2, 0); + renderer.vertex(2, 1); + renderer.vertex(3, 0); + renderer.vertex(3, 1); + renderer.endShape(); + + assert.equal(renderer.immediateMode.geometry.edges.length, 8); + done(); + }); + + test('QUAD_STRIP mode makes edges for strip outlines', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + renderer.beginShape(myp5.QUAD_STRIP); + renderer.vertex(0, 0); + renderer.vertex(0, 1); + renderer.vertex(1, 0); + renderer.vertex(1, 1); + renderer.vertex(2, 0); + renderer.vertex(2, 1); + renderer.vertex(3, 0); + renderer.vertex(3, 1); + renderer.endShape(); + + // Two full quads (2 * 4) plus two edges connecting them + assert.equal(renderer.immediateMode.geometry.edges.length, 10); + done(); + }); + }); }); From 2155899449c1959304b1b1ecdfcbfc3f5acc7038 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 25 Aug 2022 21:09:55 -0400 Subject: [PATCH 151/177] Fix typo --- src/webgl/p5.RendererGL.Immediate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 42642eeddf..63e9e661d9 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -26,7 +26,7 @@ import './p5.RenderBuffer'; * @param {Number} mode webgl primitives mode. beginShape supports the * following modes: * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS,, QUAD_STRIP, + * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, * and TESS(WEBGL only) * @chainable */ From 9efe9116d0223e3210b606aca0f1ff72e856a695 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 26 Aug 2022 12:56:15 -0400 Subject: [PATCH 152/177] Merge quads test with immediate mode test, add unit tests for other buffers --- .../webgl/geometryImmediate/index.html | 10 +- .../webgl/geometryImmediate/sketch.js | 41 +++++- .../webgl/geometryQuads/index.html | 25 ---- .../webgl/geometryQuads/sketch.js | 48 ------- test/unit/webgl/p5.RendererGL.js | 129 +++++++++++++++--- 5 files changed, 154 insertions(+), 99 deletions(-) delete mode 100644 test/manual-test-examples/webgl/geometryQuads/index.html delete mode 100644 test/manual-test-examples/webgl/geometryQuads/sketch.js diff --git a/test/manual-test-examples/webgl/geometryImmediate/index.html b/test/manual-test-examples/webgl/geometryImmediate/index.html index ddd746b4aa..3c4f912e4c 100644 --- a/test/manual-test-examples/webgl/geometryImmediate/index.html +++ b/test/manual-test-examples/webgl/geometryImmediate/index.html @@ -13,15 +13,17 @@
- 1: Black Horizontal Line
+ 1: A strip with outlined triangles, three outlines quads, a strip with outlined quads
+ TESTS: beginShape() with TRIANGLE_STRIP, QUADS, and QUAD_STRIP

+ 2: Black Horizontal Line
TESTS: line()

- 2: 3 stroked purple shapes with hollow centers
+ 3: 3 stroked purple shapes with hollow centers
TESTS: tessellation with vertex()

- 3: 4 squares Red, Blue, Grid, Grid
+ 4: 4 squares Red, Blue, Grid, Grid
TESTS: vertex with 2, 3, 4, and 5 arguments

FPS should average higher than 50 FPS on modern laptop/desktop
- \ No newline at end of file + diff --git a/test/manual-test-examples/webgl/geometryImmediate/sketch.js b/test/manual-test-examples/webgl/geometryImmediate/sketch.js index f6404ea99f..df6dc05154 100644 --- a/test/manual-test-examples/webgl/geometryImmediate/sketch.js +++ b/test/manual-test-examples/webgl/geometryImmediate/sketch.js @@ -1,6 +1,7 @@ let angle, px, py; let img; const sz = 25; +let stripColors = []; function preload() { img = loadImage('../assets/UV_Grid_Sm.jpg'); @@ -12,12 +13,34 @@ function setup() { textureMode(NORMAL); fill(63, 81, 181); strokeWeight(2); + + for (let i = 0; i < 12; i++) { + stripColors.push([random(255), random(255), random(255)]); + } } function draw() { background(250); - line(-width / 2, -180, 0, width / 2, -180, 0); + // Reference: TRIANGLE_STRIP + push(); + translate(-width / 3, -240); + drawStrip(TRIANGLE_STRIP); + pop(); + + // Test 1: QUADS + push(); + translate(0, -240); + drawStrip(QUADS); + pop(); + + // Test 2: QUAD_STRIP + push(); + translate(width / 3, -240); + drawStrip(QUAD_STRIP); + pop(); + + line(-width / 2, -160, 0, width / 2, -160, 0); ngon(5, -200, 0, 120); ngon(8, 0, 0, 120); @@ -26,6 +49,22 @@ function draw() { drawQuads(180); } +function drawStrip(mode) { + rotate(PI / 2); + scale(0.3); + translate(0, -250); + beginShape(mode); + let vertexIndex = 0; + for (let y = 0; y <= 500; y += 100) { + for (const side of [-1, 1]) { + fill(...stripColors[vertexIndex]); + vertex(side * 40, y); + vertexIndex++; + } + } + endShape(); +} + function ngon(n, x, y, d) { beginShape(TESS); for (let i = 0; i < n + 1; i++) { diff --git a/test/manual-test-examples/webgl/geometryQuads/index.html b/test/manual-test-examples/webgl/geometryQuads/index.html deleted file mode 100644 index 789e801b2b..0000000000 --- a/test/manual-test-examples/webgl/geometryQuads/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - -
- Left: A solid rectangle broken up into triangles
- TESTS: TRIANGLE_STRIP

- Center: Three rectangles
- TESTS: QUADS

- Right: A solid rectangle broken up into quads
- TESTS: QUAD_STRIP -
- - - diff --git a/test/manual-test-examples/webgl/geometryQuads/sketch.js b/test/manual-test-examples/webgl/geometryQuads/sketch.js deleted file mode 100644 index 73c398979a..0000000000 --- a/test/manual-test-examples/webgl/geometryQuads/sketch.js +++ /dev/null @@ -1,48 +0,0 @@ -let angle, px, py; -let img; -const sz = 25; - -function preload() { - img = loadImage('../assets/UV_Grid_Sm.jpg'); -} - -function setup() { - createCanvas(600, 600, WEBGL); - setAttributes('antialias', true); - fill(63, 81, 181); - strokeWeight(2); - noLoop(); -} - -function draw() { - background(250); - - // Reference: TRIANGLE_STRIP - push(); - translate(-width / 4, -250); - drawQuads(TRIANGLE_STRIP); - pop(); - - // Test 1: QUADS - push(); - translate(0, -250); - drawQuads(QUADS); - pop(); - - // Test 2: QUAD_STRIP - push(); - translate(width / 4, -250); - drawQuads(QUAD_STRIP); - pop(); -} - -function drawQuads(mode) { - beginShape(mode); - for (let y = 0; y <= 500; y += 100) { - for (const side of [-1, 1]) { - fill(random(255), random(255), random(255)); - vertex(side * 40, y); - } - } - endShape(); -} diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 64c548d714..0eef35c050 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -526,19 +526,63 @@ suite('p5.RendererGL', function() { suite('beginShape() in WEBGL mode', function() { test('QUADS mode converts into triangles', function(done) { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.textureMode(myp5.NORMAL); renderer.beginShape(myp5.QUADS); - renderer.vertex(0, 0); - renderer.vertex(0, 1); - renderer.vertex(1, 0); - renderer.vertex(1, 1); - - renderer.vertex(2, 0); - renderer.vertex(2, 1); - renderer.vertex(3, 0); - renderer.vertex(3, 1); + renderer.fill(255, 0, 0); + renderer.normal(0, 1, 2); + renderer.vertex(0, 0, 0, 0, 0); + renderer.fill(0, 255, 0); + renderer.normal(3, 4, 5); + renderer.vertex(0, 1, 1, 0, 1); + renderer.fill(0, 0, 255); + renderer.normal(6, 7, 8); + renderer.vertex(1, 0, 2, 1, 0); + renderer.fill(255, 0, 255); + renderer.normal(9, 10, 11); + renderer.vertex(1, 1, 3, 1, 1); + + renderer.fill(255, 0, 0); + renderer.normal(12, 13, 14); + renderer.vertex(2, 0, 4, 0, 0); + renderer.fill(0, 255, 0); + renderer.normal(15, 16, 17); + renderer.vertex(2, 1, 5, 0, 1); + renderer.fill(0, 0, 255); + renderer.normal(18, 19, 20); + renderer.vertex(3, 0, 6, 1, 0); + renderer.fill(255, 0, 255); + renderer.normal(21, 22, 23); + renderer.vertex(3, 1, 7, 1, 1); renderer.endShape(); - const expected = [ + const expectedVerts = [ + [0, 0, 0], + [0, 1, 1], + [1, 0, 2], + + [1, 0, 2], + [0, 1, 1], + [1, 1, 3], + + [2, 0, 4], + [2, 1, 5], + [3, 0, 6], + + [3, 0, 6], + [2, 1, 5], + [3, 1, 7] + ]; + assert.equal( + renderer.immediateMode.geometry.vertices.length, + expectedVerts.length + ); + expectedVerts.forEach(function([x, y, z], i) { + assert.equal(renderer.immediateMode.geometry.vertices[i].x, x); + assert.equal(renderer.immediateMode.geometry.vertices[i].y, y); + assert.equal(renderer.immediateMode.geometry.vertices[i].z, z); + }); + + const expectedUVs = [ [0, 0], [0, 1], [1, 0], @@ -547,22 +591,65 @@ suite('p5.RendererGL', function() { [0, 1], [1, 1], - [2, 0], - [2, 1], - [3, 0], + [0, 0], + [0, 1], + [1, 0], - [3, 0], - [2, 1], - [3, 1] + [1, 0], + [0, 1], + [1, 1] + ].flat(); + assert.deepEqual(renderer.immediateMode.geometry.uvs, expectedUVs); + + const expectedColors = [ + [1, 0, 0, 1], + [0, 1, 0, 1], + [0, 0, 1, 1], + + [0, 0, 1, 1], + [0, 1, 0, 1], + [1, 0, 1, 1], + + [1, 0, 0, 1], + [0, 1, 0, 1], + [0, 0, 1, 1], + + [0, 0, 1, 1], + [0, 1, 0, 1], + [1, 0, 1, 1] + ].flat(); + assert.deepEqual( + renderer.immediateMode.geometry.vertexColors, + expectedColors + ); + + const expectedNormals = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + + [6, 7, 8], + [3, 4, 5], + [9, 10, 11], + + [12, 13, 14], + [15, 16, 17], + [18, 19, 20], + + [18, 19, 20], + [15, 16, 17], + [21, 22, 23] ]; assert.equal( - renderer.immediateMode.geometry.vertices.length, - expected.length + renderer.immediateMode.geometry.vertexNormals.length, + expectedNormals.length ); - expected.forEach(function([x, y], i) { - assert.equal(renderer.immediateMode.geometry.vertices[i].x, x); - assert.equal(renderer.immediateMode.geometry.vertices[i].y, y); + expectedNormals.forEach(function([x, y, z], i) { + assert.equal(renderer.immediateMode.geometry.vertexNormals[i].x, x); + assert.equal(renderer.immediateMode.geometry.vertexNormals[i].y, y); + assert.equal(renderer.immediateMode.geometry.vertexNormals[i].z, z); }); + done(); }); From b4c60d0e3f1bb6d4f8136472c768048353a1f252 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 26 Aug 2022 13:15:01 -0400 Subject: [PATCH 153/177] Fix path to stats script --- test/manual-test-examples/webgl/stats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/manual-test-examples/webgl/stats.js b/test/manual-test-examples/webgl/stats.js index 2f3a23c9f5..1444a81dfa 100644 --- a/test/manual-test-examples/webgl/stats.js +++ b/test/manual-test-examples/webgl/stats.js @@ -10,6 +10,6 @@ requestAnimationFrame(loop); }); }; - script.src = 'http://rawgit.com/mrdoob/stats.js/main/build/stats.min.js'; + script.src = 'http://rawgit.com/mrdoob/stats.js/9d23c79/build/stats.min.js'; document.head.appendChild(script); })(); From e5bec489d69a56361a1ffeb159a7bcdef2563297 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 28 Aug 2022 08:56:47 -0400 Subject: [PATCH 154/177] Fix vertex ordering for beginShape(QUADS) in WebGL mode --- src/webgl/p5.RendererGL.Immediate.js | 23 +++++++++++-------- .../webgl/geometryImmediate/sketch.js | 18 ++++++++++++++- test/unit/webgl/p5.RendererGL.js | 16 ++++++------- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 63e9e661d9..2bfc694ba8 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -62,17 +62,20 @@ p5.RendererGL.prototype.vertex = function(x, y) { if (this.immediateMode.shapeMode === constants.QUADS) { // A finished quad turned into triangles should leave 6 vertices in the // buffer: - // 0--2 0--2 3 - // | | --> | / / | - // 1--3 1 4--5 + // 0--3 0 3--5 + // | | --> | \ \ | + // 1--2 1--2 4 // When vertex index 3 is being added, add the necessary duplicates. if (this.immediateMode.geometry.vertices.length % 6 === 3) { for (const key in immediateBufferStrides) { const stride = immediateBufferStrides[key]; const buffer = this.immediateMode.geometry[key]; buffer.push( - ...buffer.slice(buffer.length - stride, buffer.length), - ...buffer.slice(buffer.length - 2 * stride, buffer.length - stride) + ...buffer.slice( + buffer.length - 3 * stride, + buffer.length - 2 * stride + ), + ...buffer.slice(buffer.length - stride, buffer.length) ); } } @@ -266,14 +269,14 @@ p5.RendererGL.prototype._calculateEdges = function( break; case constants.QUADS: // Quads have been broken up into two triangles by `vertex()`: - // 0---2 3 - // | / / | - // 1 4---5 + // 0 3--5 + // | \ \ | + // 1--2 4 for (i = 0; i < verts.length - 5; i += 6) { res.push([i, i + 1]); - res.push([i, i + 2]); - res.push([i + 4, i + 5]); + res.push([i + 1, i + 2]); res.push([i + 3, i + 5]); + res.push([i + 4, i + 5]); } break; case constants.QUAD_STRIP: diff --git a/test/manual-test-examples/webgl/geometryImmediate/sketch.js b/test/manual-test-examples/webgl/geometryImmediate/sketch.js index df6dc05154..27ce74a562 100644 --- a/test/manual-test-examples/webgl/geometryImmediate/sketch.js +++ b/test/manual-test-examples/webgl/geometryImmediate/sketch.js @@ -56,7 +56,23 @@ function drawStrip(mode) { beginShape(mode); let vertexIndex = 0; for (let y = 0; y <= 500; y += 100) { - for (const side of [-1, 1]) { + let sides = [-1, 1]; + if (mode === QUADS && y % 200 !== 0) { + // QUAD_STRIP and TRIANGLE_STRIP need the vertices of each shared side + // ordered in the same way: + // 0--2--4--6 + // | | | | ⬇️ + // 1--3--5--7 + // + // ...but QUADS orders vertices in a consisten CCW or CW manner around + // each quad, meaning each side will be in the reverse order of the + // previous: + // 0--3 4--7 + // | | | | 🔄 + // 1--2 5--6 + sides.reverse(); + } + for (const side of sides) { fill(...stripColors[vertexIndex]); vertex(side * 40, y); vertexIndex++; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 0eef35c050..330cf444bf 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -560,16 +560,16 @@ suite('p5.RendererGL', function() { [0, 1, 1], [1, 0, 2], + [0, 0, 0], [1, 0, 2], - [0, 1, 1], [1, 1, 3], [2, 0, 4], [2, 1, 5], [3, 0, 6], + [2, 0, 4], [3, 0, 6], - [2, 1, 5], [3, 1, 7] ]; assert.equal( @@ -587,16 +587,16 @@ suite('p5.RendererGL', function() { [0, 1], [1, 0], + [0, 0], [1, 0], - [0, 1], [1, 1], [0, 0], [0, 1], [1, 0], + [0, 0], [1, 0], - [0, 1], [1, 1] ].flat(); assert.deepEqual(renderer.immediateMode.geometry.uvs, expectedUVs); @@ -606,16 +606,16 @@ suite('p5.RendererGL', function() { [0, 1, 0, 1], [0, 0, 1, 1], + [1, 0, 0, 1], [0, 0, 1, 1], - [0, 1, 0, 1], [1, 0, 1, 1], [1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1], + [1, 0, 0, 1], [0, 0, 1, 1], - [0, 1, 0, 1], [1, 0, 1, 1] ].flat(); assert.deepEqual( @@ -628,16 +628,16 @@ suite('p5.RendererGL', function() { [3, 4, 5], [6, 7, 8], + [0, 1, 2], [6, 7, 8], - [3, 4, 5], [9, 10, 11], [12, 13, 14], [15, 16, 17], [18, 19, 20], + [12, 13, 14], [18, 19, 20], - [15, 16, 17], [21, 22, 23] ]; assert.equal( From 5890807078a26a0af4f5a993f5bda7a4764b4949 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 28 Aug 2022 15:12:31 -0400 Subject: [PATCH 155/177] Shrink image used for tint test to speed them up --- test/unit/assets/cat-with-hole.png | Bin 85478 -> 2817 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/unit/assets/cat-with-hole.png b/test/unit/assets/cat-with-hole.png index 249b38010cd13061ecbe6a48019217b5298eded2..65b1b87f13465c16399506966b9b11e48f3373e8 100644 GIT binary patch literal 2817 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 literal 85478 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 Date: Mon, 29 Aug 2022 12:40:28 -0700 Subject: [PATCH 156/177] Add more stewards --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af51776078..6cad85a68d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Overall | [@qianqianye](https://github.com/qianqianye) | | [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | -| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese), [@aahdee](https://github.com/aahdee) | +| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese), [@aahdee](https://github.com/aahdee), [@paulaxisabel](https://github.com/paulaxisabel) | | [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth), [@davepagurek](https://github.com/davepagurek), [@jeffawang](https://github.com/jeffawang) | | [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | | [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye), [@SamirDhoke](https://github.com/SamirDhoke) | @@ -75,7 +75,7 @@ Anyone interested can volunteer to be a steward! There are no specific requireme | [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko), [@KleoP](https://github.com/KleoP) | [IO](https://github.com/processing/p5.js/tree/main/src/io) | [@limzykenneth](https://github.com/limzykenneth) | | [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth), [@jeffawang](https://github.com/jeffawang), [@AdilRabbani](https://github.com/AdilRabbani) | -| [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe), [@SarveshLimaye](https://github.com/SarveshLimaye) | +| [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe), [@SarveshLimaye](https://github.com/SarveshLimaye), [@paulaxisabel](https://github.com/paulaxisabel) | | [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | | [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor); [@davepagurek](https://github.com/davepagurek); [@jeffawang](https://github.com/jeffawang); [@AdilRabbani](https://github.com/AdilRabbani) | | Build Process/Unit Testing | [@outofambit](https://github.com/outofambit), [@kungfuchicken](https://github.com/kungfuchicken) | From 00d129d8546f5c0a03cd768a38e115d7c8f7ca18 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 30 Aug 2022 12:12:22 +0100 Subject: [PATCH 157/177] Write single document contributor guidelines --- .../contributing_documentation.md | 5 +- contributor_docs/contributor_guidelines.md | 210 +++++++++++++++++- 2 files changed, 209 insertions(+), 6 deletions(-) diff --git a/contributor_docs/contributing_documentation.md b/contributor_docs/contributing_documentation.md index bc69502a1d..fc52123878 100644 --- a/contributor_docs/contributing_documentation.md +++ b/contributor_docs/contributing_documentation.md @@ -1,3 +1,5 @@ +# Contributing Documentation + Documentation is essential for new learners and experienced programmers alike. It helps make our community inclusive by extending a friendly hand to those who are less familiar with p5.js. It also helps us find the bugs and issues with the code itself, because we test and try things out as we document. There are several ways to contribute to documentation: @@ -31,6 +33,3 @@ While the examples in the reference are meant to be very simplistic snippets of * All discussion happens on github issues, so there's no slack/gitter/etc channel you need to join. * Add your name to the [contributors list](https://github.com/processing/p5.js#contributors) in the readme.md file! Instructions [here](https://github.com/processing/p5.js/issues/2309). * And of course, if you're more of a bug fixer kind of person, feel free to jump into any of the [issues](https://github.com/processing/p5.js/issues)! - -Welcome! We're so glad you're here! -❤️ the p5.js community diff --git a/contributor_docs/contributor_guidelines.md b/contributor_docs/contributor_guidelines.md index 4a17855ca6..a2532e74e7 100644 --- a/contributor_docs/contributor_guidelines.md +++ b/contributor_docs/contributor_guidelines.md @@ -6,8 +6,31 @@ If you are looking to contribute outside of the p5.js repositories (writing tuto This is a fairly long and comprehensive document but we will try to deliniate all steps and points as clearly as possible. Do utilize the table of contents, the browser search functionality (`Ctrl + f` or `Cmd + f`) to find sections relevant to you. Feel free to skip sections if they are not relevant to your planned contributions as well. # Table of Contents -- All about issues -- Pull requests +- [All about issues](#all-about-issues) + - [What are issues?](#what-are-issues) + - [Issue templates](#issue-templates) + - [Found a bug](#found-a-bug) + - [Existing Feature Enhancement](#existing-feature-enhancement) + - [New Feature Request](#new-feature-request) + - [Discussion](#discussion) +- [Working on p5.js codebase](#working-on-p5js-codebase) + - [Using the Github edit functionality](#using-the-github-edit-functionality) + - [Forking p5.js and working from your fork](#forking-p5js-and-working-from-your-fork) + - [Codebase breakdown](#codebase-breakdown) + - [Build setup](#build-setup) + - [Git workflow](#git-workflow) + - [Source code](#source-code) + - [Unit tests](#unit-tests) + - [Inline documentation](#inline-documentation) + - [Internationalization](#internationalization) + - [Accessibility](#accessibility) + - [Code standard](#code-standard) + - [Design principles](#design-principles) +- [Pull requests](#pull-requests) + - [Creating a pull request](#creating-a-pull-request) + - [Pull request information](#pull-request-information) + - [Rebase and resolve conflicts](#rebase-and-resolve-conflicts) + - [Discuss and amend](#discuss-and-amend) --- # All about issues @@ -21,6 +44,187 @@ While an issue can be opened for a wide variety of reasons, for p5.js' repos we We have created easy to use issue templates to aid you in deciding whether a topic should be a Github issue or it should be posted somewhere else! ## Issue templates +p5.js' issue templates not only makes it easier for stewards and maintainers to understand and review issues, it also makes it simpler for you to file the relevant issue and receive a reply faster. Although they are called templates, from your perspective, it will just be like filling in a simple form where all the different fields of the form are the potentially important information that issue reviewers will need to properly diagnose your issue. + +To file a new issue, simply go to the "Issues" tab on the p5.js repo and click on the "New issue" button (usually in green and on the right side). Once you have clicked that, you will be presented with several different options, each of which either correspond to a relevant issue template or redirect you to the relevant place to file your question. You should choose the most relevant option out of all that are presented to ensure your issue can receive the right attention promptly. We will cover the issue templates that applies to p5.js below, for other repos, please check their respective contributor documentation. + +### "Found a bug" +When you encounter possible incorrect behaviour in p5.js or something not behaving as described in the documentation, this is the template you should use. Please note that if you are trying to debug your own code or figure out why your sketch is not behaving as you expected and you think it may be a problem with your code, you should ask on the [forum](https://discourse.processing.org) instead. If it is later determined your problem did stem from p5.js, you can always open an issue and use this template then. + +There are few fields for you to fill in for this template: +1. "Most appropriate sub-area of p5.js?" - This helps the appropriate stewards identify and respond to your issue. This will automatically tag the issue with the relevant [labels](./issue_labels.md). +2. "p5.js version" - You can find the p5.js version number in either the ` + + + + + diff --git a/test/manual-test-examples/p5.Font/textInRect/sketch.js b/test/manual-test-examples/p5.Font/textInRect/sketch.js new file mode 100644 index 0000000000..d7d5fa0f49 --- /dev/null +++ b/test/manual-test-examples/p5.Font/textInRect/sketch.js @@ -0,0 +1,81 @@ +let xpos = 50; +let ypos = 100; +let str = + 'One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve Thirteen Fourteen Fifteen Sixteen Seventeen Eighteen Nineteen Twenty Twenty-one Twenty-two Twenty-three Twenty-four Twenty-five Twenty-six Twenty-seven Twenty-eight Twenty-nine Thirty Thirty-one Thirty-two Thirty-three Thirty-four Thirty-five Thirty-six Thirty-seven Thirty-eight Thirty-nine Forty Forty-one Forty-two Forty-three Forty-four Forty-five Forty-six Forty-seven Forty-eight Forty-nine Fifty Fifty-one Fifty-two Fifty-three'; + +function setup() { + createCanvas(1050, 800); + background(245); + + let ta = textAscent(); + + textAlign(CENTER, TOP); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + xpos += 250; + + textAlign(CENTER, CENTER); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + xpos += 250; + + textAlign(CENTER, BOTTOM); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + xpos += 250; + + textAlign(CENTER, BASELINE); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + + textSize(18); + textAlign(CENTER, TOP); + text('TOP', 150, height / 2 - 40); + text('CENTER', 400, height / 2 - 40); + text('BOTTOM', 650, height / 2 - 40); + text('BASELINE', 900, height / 2 - 40); + textSize(12); + + xpos = 50; + ypos += 400; + + textAlign(CENTER, TOP); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + xpos += 250; + + textAlign(CENTER, CENTER); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + xpos += 250; + + textAlign(CENTER, BOTTOM); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + xpos += 250; + + textAlign(CENTER, BASELINE); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + + textSize(18); + textAlign(CENTER, TOP); + text('TOP', 150, height / 2 - 40); + text('CENTER', 400, height / 2 - 40); + text('BOTTOM', 650, height / 2 - 40); + text('BASELINE', 900, height / 2 - 40); + text('TOP', 150, ypos + 270); + text('CENTER', 400, ypos + 270); + text('BOTTOM', 650, ypos + 270); + text('BASELINE', 900, ypos + 270); + + fill(255); + noStroke(); + textSize(24); + + rect(0, height / 2, width, 15); + fill(0); + textAlign(LEFT, TOP); + text('text(s, x, y, w, h)', 20, 40); + text('text(s, x, y, w) [no height]', 20, height / 2 + 40); +} From eda2e43ec9004de8c546ad01409f98dfe8ee3321 Mon Sep 17 00:00:00 2001 From: Qianqian Ye Date: Tue, 6 Sep 2022 11:40:15 -0700 Subject: [PATCH 163/177] Remove VIDEO and AUDIO from reference --- src/dom/dom.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/dom/dom.js b/src/dom/dom.js index 8b0e59a193..af53e34793 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -1318,17 +1318,8 @@ p5.prototype.createAudio = function(src, callback) { /** CAMERA STUFF **/ -/** - * @property {String} VIDEO - * @final - * @category Constants - */ p5.prototype.VIDEO = 'video'; -/** - * @property {String} AUDIO - * @final - * @category Constants - */ + p5.prototype.AUDIO = 'audio'; // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia From 979353a293c1766c7c591a7d78b81ead05c5073f Mon Sep 17 00:00:00 2001 From: ShenpaiSharma Date: Wed, 7 Sep 2022 16:05:50 +0530 Subject: [PATCH 164/177] Added round corner property for rect in WebGL mode --- src/webgl/3d_primitives.js | 156 +++++++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 43 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 090a89479e..79302a2d5d 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -1180,56 +1180,126 @@ p5.RendererGL.prototype.arc = function(args) { }; p5.RendererGL.prototype.rect = function(args) { - const perPixelLighting = this._pInst._glAttributes.perPixelLighting; const x = args[0]; const y = args[1]; const width = args[2]; const height = args[3]; - const detailX = args[4] || (perPixelLighting ? 1 : 24); - const detailY = args[5] || (perPixelLighting ? 1 : 16); - const gId = `rect|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { - const _rect = function() { - for (let i = 0; i <= this.detailY; i++) { - const v = i / this.detailY; - for (let j = 0; j <= this.detailX; j++) { - const u = j / this.detailX; - const p = new p5.Vector(u, v, 0); - this.vertices.push(p); - this.uvs.push(u, v); + + if (typeof args[4] === 'undefined') { + // Use the retained mode for drawing rectangle, + // if args for rounding rectangle is not provided by user. + const perPixelLighting = this._pInst._glAttributes.perPixelLighting; + const detailX = args[4] || (perPixelLighting ? 1 : 24); + const detailY = args[5] || (perPixelLighting ? 1 : 16); + const gId = `rect|${detailX}|${detailY}`; + if (!this.geometryInHash(gId)) { + const _rect = function() { + for (let i = 0; i <= this.detailY; i++) { + const v = i / this.detailY; + for (let j = 0; j <= this.detailX; j++) { + const u = j / this.detailX; + const p = new p5.Vector(u, v, 0); + this.vertices.push(p); + this.uvs.push(u, v); + } } - } - // using stroke indices to avoid stroke over face(s) of rectangle - if (detailX > 0 && detailY > 0) { - this.strokeIndices = [ - [0, detailX], - [detailX, (detailX + 1) * (detailY + 1) - 1], - [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY], - [(detailX + 1) * detailY, 0] - ]; - } - }; - const rectGeom = new p5.Geometry(detailX, detailY, _rect); - rectGeom - .computeFaces() - .computeNormals() - ._makeTriangleEdges() - ._edgesToVertices(); - this.createBuffers(gId, rectGeom); - } + // using stroke indices to avoid stroke over face(s) of rectangle + if (detailX > 0 && detailY > 0) { + this.strokeIndices = [ + [0, detailX], + [detailX, (detailX + 1) * (detailY + 1) - 1], + [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY], + [(detailX + 1) * detailY, 0] + ]; + } + }; + const rectGeom = new p5.Geometry(detailX, detailY, _rect); + rectGeom + .computeFaces() + .computeNormals() + ._makeTriangleEdges() + ._edgesToVertices(); + this.createBuffers(gId, rectGeom); + } - // only a single rectangle (of a given detail) is cached: a square with - // opposite corners at (0,0) & (1,1). - // - // before rendering, this square is scaled & moved to the required location. - const uMVMatrix = this.uMVMatrix.copy(); - try { - this.uMVMatrix.translate([x, y, 0]); - this.uMVMatrix.scale(width, height, 1); + // only a single rectangle (of a given detail) is cached: a square with + // opposite corners at (0,0) & (1,1). + // + // before rendering, this square is scaled & moved to the required location. + const uMVMatrix = this.uMVMatrix.copy(); + try { + this.uMVMatrix.translate([x, y, 0]); + this.uMVMatrix.scale(width, height, 1); + + this.drawBuffers(gId); + } finally { + this.uMVMatrix = uMVMatrix; + } + } else { + // Use Immediate mode to round the rectangle corner, + // if args for rounding corners is provided by user + let tl = args[4]; + let tr = typeof args[5] === 'undefined' ? tl : args[5]; + let br = typeof args[6] === 'undefined' ? tr : args[6]; + let bl = typeof args[7] === 'undefined' ? br : args[7]; + + let a = x; + let b = y; + let c = width; + let d = height; + + c += a; + d += b; + + if (a > c) { + const temp = a; + a = c; + c = temp; + } - this.drawBuffers(gId); - } finally { - this.uMVMatrix = uMVMatrix; + if (b > d) { + const temp = b; + b = d; + d = temp; + } + + const maxRounding = Math.min((c - a) / 2, (d - b) / 2); + if (tl > maxRounding) tl = maxRounding; + if (tr > maxRounding) tr = maxRounding; + if (br > maxRounding) br = maxRounding; + if (bl > maxRounding) bl = maxRounding; + + let x1 = a; + let y1 = b; + let x2 = c; + let y2 = d; + + this.beginShape(); + if (tr !== 0) { + this.vertex(x2 - tr, y1); + this.quadraticVertex(x2, y1, x2, y1 + tr); + } else { + this.vertex(x2, y1); + } + if (br !== 0) { + this.vertex(x2, y2 - br); + this.quadraticVertex(x2, y2, x2 - br, y2); + } else { + this.vertex(x2, y2); + } + if (bl !== 0) { + this.vertex(x1 + bl, y2); + this.quadraticVertex(x1, y2, x1, y2 - bl); + } else { + this.vertex(x1, y2); + } + if (tl !== 0) { + this.vertex(x1, y1 + tl); + this.quadraticVertex(x1, y1, x1 + tl, y1); + } else { + this.vertex(x1, y1); + } + this.endShape(constants.CLOSE); } return this; }; From e937fc224a8a923559c562795d1dd033ac3fe861 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 9 Sep 2022 16:57:56 -0400 Subject: [PATCH 165/177] Fix WebGL blending bugs --- src/core/rendering.js | 3 +- src/webgl/material.js | 10 ++++- src/webgl/p5.RendererGL.js | 1 + test/unit/webgl/p5.RendererGL.js | 64 +++++++++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/core/rendering.js b/src/core/rendering.js index c2526ad726..391987a409 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -252,7 +252,8 @@ p5.prototype.createGraphics = function(w, h, renderer) { * min(A*factor, B). *
  • LIGHTEST - only the lightest colour succeeds: C = * max(A*factor, B).
  • - *
  • DIFFERENCE - subtract colors from underlying image.
  • + *
  • DIFFERENCE - subtract colors from underlying image. + * (2D)
  • *
  • EXCLUSION - similar to DIFFERENCE, but less * extreme.
  • *
  • MULTIPLY - multiply the colors, result will always be diff --git a/src/webgl/material.js b/src/webgl/material.js index 6694427a3b..0157edf294 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1054,7 +1054,10 @@ p5.RendererGL.prototype._applyColorBlend = function(colors) { const isTexture = this.drawMode === constants.TEXTURE; const doBlend = - isTexture || colors[colors.length - 1] < 1.0 || this._isErasing; + isTexture || + this.curBlendMode !== constants.BLEND || + colors[colors.length - 1] < 1.0 || + this._isErasing; if (doBlend !== this._isBlending) { if ( @@ -1085,10 +1088,13 @@ p5.RendererGL.prototype._applyBlendMode = function() { const gl = this.GL; switch (this.curBlendMode) { case constants.BLEND: - case constants.ADD: gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); break; + case constants.ADD: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE); + break; case constants.REMOVE: gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT); gl.blendFunc(gl.SRC_ALPHA, gl.DST_ALPHA); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 17a61130cd..1023a826eb 100755 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1032,6 +1032,7 @@ p5.RendererGL.prototype.push = function() { properties.drawMode = this.drawMode; properties._currentNormal = this._currentNormal; + properties.curBlendMode = this.curBlendMode; return style; }; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 330cf444bf..822c3013f8 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -437,7 +437,7 @@ suite('p5.RendererGL', function() { test('blendModes change pixel colors as expected', function(done) { myp5.createCanvas(10, 10, myp5.WEBGL); myp5.noStroke(); - assert.deepEqual([133, 69, 191, 255], mixAndReturn(myp5.ADD, 255)); + assert.deepEqual([122, 0, 122, 255], mixAndReturn(myp5.ADD, 0)); assert.deepEqual([0, 0, 255, 255], mixAndReturn(myp5.REPLACE, 255)); assert.deepEqual([133, 255, 133, 255], mixAndReturn(myp5.SUBTRACT, 255)); assert.deepEqual([255, 0, 255, 255], mixAndReturn(myp5.SCREEN, 0)); @@ -447,6 +447,68 @@ suite('p5.RendererGL', function() { assert.deepEqual([0, 0, 0, 255], mixAndReturn(myp5.DARKEST, 255)); done(); }); + + test('blendModes match 2D mode', function(done) { + myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.setAttributes({ alpha: true }); + const ref = myp5.createGraphics(myp5.width, myp5.height); + ref.translate(ref.width / 2, ref.height / 2); // Match WebGL mode + + const testBlend = function(target, colorA, colorB, mode) { + target.clear(); + target.push(); + target.background(colorA); + target.blendMode(mode); + target.noStroke(); + target.fill(colorB); + target.rectMode(target.CENTER); + target.rect(0, 0, target.width, target.height); + target.pop(); + return target.get(0, 0); + }; + + const assertSameIn2D = function(colorA, colorB, mode) { + const refColor = testBlend(myp5, colorA, colorB, mode); + const webglColor = testBlend(ref, colorA, colorB, mode); + if (refColor[3] === 0) { + assert.equal(webglColor[3], 0); + } else { + assert.deepEqual( + refColor, + webglColor, + `Blending ${colorA} with ${colorB} using ${mode}` + ); + } + }; + + const red = '#F53'; + const blue = '#13F'; + assertSameIn2D(red, blue, myp5.BLEND); + assertSameIn2D(red, blue, myp5.ADD); + assertSameIn2D(red, blue, myp5.DARKEST); + assertSameIn2D(red, blue, myp5.LIGHTEST); + assertSameIn2D(red, blue, myp5.EXCLUSION); + assertSameIn2D(red, blue, myp5.MULTIPLY); + assertSameIn2D(red, blue, myp5.SCREEN); + assertSameIn2D(red, blue, myp5.REPLACE); + assertSameIn2D(red, blue, myp5.REMOVE); + done(); + }); + + test('blendModes are included in push/pop', function(done) { + myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.blendMode(myp5.MULTIPLY); + myp5.push(); + myp5.blendMode(myp5.ADD); + assert.equal(myp5._renderer.curBlendMode, myp5.ADD, 'Changed to ADD'); + myp5.pop(); + assert.equal( + myp5._renderer.curBlendMode, + myp5.MULTIPLY, + 'Resets to MULTIPLY' + ); + done(); + }); }); suite('BufferDef', function() { From d9d781c391712858749ac5c44286c94607fe3021 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 9 Sep 2022 21:00:36 -0400 Subject: [PATCH 166/177] Add texture coordinates to WebGL rounded rects --- src/webgl/3d_primitives.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index bf31695121..9e258c2bbe 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -1336,6 +1336,14 @@ p5.RendererGL.prototype.rect = function(args) { } else { this.vertex(x1, y1); } + + this.immediateMode.geometry.uvs.length = 0; + for (const vert of this.immediateMode.geometry.vertices) { + const u = (vert.x - x1) / width; + const v = (vert.y - y1) / height; + this.immediateMode.geometry.uvs.push(u, v); + } + this.endShape(constants.CLOSE); } return this; From c01a7efd6eb5c1a24780b0eb23b3ab37a8868d6a Mon Sep 17 00:00:00 2001 From: Gracia-zhang <70793865+Gracia-zhang@users.noreply.github.com> Date: Sat, 10 Sep 2022 19:20:14 -0400 Subject: [PATCH 167/177] Create graciazhang_gsoc_2022.md Add GSoC 2022 wrap-up --- .../project_wrapups/graciazhang_gsoc_2022.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 contributor_docs/project_wrapups/graciazhang_gsoc_2022.md diff --git a/contributor_docs/project_wrapups/graciazhang_gsoc_2022.md b/contributor_docs/project_wrapups/graciazhang_gsoc_2022.md new file mode 100644 index 0000000000..d1d1f52951 --- /dev/null +++ b/contributor_docs/project_wrapups/graciazhang_gsoc_2022.md @@ -0,0 +1,119 @@ +# p5 /teach reorganize & update +#### by Gracia Zhang ([@Gracia-zhang](https://github.com/Gracia-zhang)), mentored by ([@yinhwa](https://github.com/yinhwa)) + + +## Overview + + +Updated 12 new posts that were submitted in the past two years on Teach Page, using yaml and Handlebars. Re-design the user interface(UI) and the experience of the Teach Page with new functions and features. And responsible for the front-end development of this web page. The project was mentored by Inhwa Yeom. + + +* Project Page: https://p5js.org/teach/ + + +* Final Pull Request(UI/UX update): https://github.com/processing/p5.js-website/pull/1275 + + +## Goal & Approach + + +This project is an update and extension of Inhwa's project “p5 for 50+ teaching”. She has archived and visualized workshops, classes and relevant materials to better provide the community with access to education-related resources. + + +In this project, I updated the posts based on the recent submission form first to be familiar with the original /Teach and to research the user through the submitted forms. Based on user studies, I intend to bring more learners & diversity on /teach page by optimizing the submission form with a new form for learners who want to share and a new section "Upcoming Workshops". Inhwa and I hope that this new section will bring in more willing learners and give more teachers the opportunity to share their workshops, classes, etc. + + +## Updating posts + + +### Screening the submission forms for the last two years + + +#### 1)Filter unpublishable contents +-the answer in the spreadsheet doesn't seem like a full version. +-filled the form up with unrelated contents. + + +#### 2)Update the other contents. +-updated with 12 filtered contents in yaml file. +-added the corresponding model box for each of the 12 new posts. + +* Teach Page Rebasing Pull Request: https://github.com/processing/p5.js-website/pull/1245 + +* Teach Page Posts Update Pull Request: https://github.com/processing/p5.js-website/pull/1249 + + +## UI/UX Design + +Besides the specific UI/UX changes below, I also proposed an issue in processing/p5.js-website about the overall structural changes to enhance the user experience.[#1250](https://github.com/processing/p5.js-website/issues/1250) + + +![](https://drive.google.com/uc?export=veiw&id=1r-wriIvnOPLL6G-ihrL-9wOSEOKfY7l3) + + +#### 1)Upcoming Workshops + +Following the original UI style, I used a combination of banners and headers to present the new workshops to guarantee accessibility. Also, to make it easier for users to plan their time, I have added workshop start time in this section. + + +#### 2)Corresponding Tags + +In order to allow users to see the results of their selection more visually after selecting the filtering options, I added a tags column to the right of each posts and added a color change corresponding to being selected. + + +## Submission Questionnaire Re-Design + +I adapted the original Submission Form specifically for learners. +With the help of Saber and Inhwa, I also updated the parts of the original form that could be ambiguous, to try to avoid misunderstandings and irrelevant content when the sharers fill out the form. [Updated form.](https://forms.gle/GVLrxrvuBTpSTzgCA) + + +### Updating Submission Form for Learners + +#### 1)In the guidelines: + +- Changed the guideline for potential learners and added a section to encourage learners to share. +- Added a note for non-English submissions to remind submitters that their responses will be automatically translated into English. + +#### 2)For learners to share their experiences: + +- Added a question to differentiate teachers and learners. +- Removed the question about teaching goal and method. +- Added questions about learning feelings and difficulties encountered. + +### New Submission Form for Upcoming Workshops + +For the new section "Upcoming workshops", I designed a new form for the submission, including basic information about the workshop and the teacher. [New form.](https://forms.gle/mP9oFpeA4SMWoi446) + +## Implementation + +Languages: HTML, CSS, jQuery + + +- Created an “upcoming workshops” section in the HTML file. +- Added script for showing/hiding each banner when different titles are clicked. +- Added HTML & jQuery & CSS codes for the preliminary versions of corresponding tags +- Edited HTML & jQuery & CSS codes for the final version of UI/UX design +- Added Css code for responsive web design on mobile devices. + +Upcoming Workshops are still on open call for specific engagement content.[#1277](https://github.com/processing/p5.js-website/issues/1277) + +## Future Plan & Sustainability + +My future contribution plans +- Add Chinese translation (As a Chinese Translation Stewards: [#1220](https://github.com/processing/p5.js-website/issues/1220)) +- Keep updating posts and upcoming workshops +- Help with the website user experience not limited to /teach.([#1250](https://github.com/processing/p5.js-website/issues/1250)) +- Start updating the workshops from an open call about “Upcoming workshops” and Learner-friendly form to gather more teachers and learners to join the /teach contribution.([#1277](https://github.com/processing/p5.js-website/issues/1277)) + + +For Future Contributors: +- Add Spanish, Japanese, Hindi translation +- Gather more teaching experiences from around the world! +- Bring more ideas to keep the page visible and useful + + +## Acknowledgements + +The first person I want to thank is Inhwa. I have to say, I looooove Inhwa as my mentor!!! I am a beginner in front-end and she helped me a lot throughout the GSoC project in every way. I couldn't have completed my project without her help. She encouraged me when I was hesitant about the direction, and explained in detail when I had problems with the code. She is both my mentor and my friend! + +I would also like to thank Saber and Qianqian.The discussion with them about education has been very profitable and I have a more solid idea about my project. From fb8b6ad29c792383b83c953efb5c7327ed8a3f31 Mon Sep 17 00:00:00 2001 From: Gracia-zhang <70793865+Gracia-zhang@users.noreply.github.com> Date: Sat, 10 Sep 2022 20:59:39 -0400 Subject: [PATCH 168/177] Update README.md Add Gracia's wrap up link for GSoC 2022 --- contributor_docs/project_wrapups/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributor_docs/project_wrapups/README.md b/contributor_docs/project_wrapups/README.md index bc72124ce8..2cdcdd0731 100644 --- a/contributor_docs/project_wrapups/README.md +++ b/contributor_docs/project_wrapups/README.md @@ -5,6 +5,9 @@ This folder contains wrap-up reports from p5.js related [Google Summer of Code]( ## Google Summer of Code +### Google Summer of Code 2022 +* [p5 /teach reorganize & update](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/graciazhang_gsoc_2022.md) by Gracia Zhang, 2022 + ### Google Summer of Code 2021 * [Adding Alt Text to the p5.js Website](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/katiejliu_gsoc_2021.md) by Katie Liu, 2021 * [Adding to p5.js Friendly Error System](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/shantanuKaushik_gsoc_2021.md) by Shantanu Kaushik, 2021 From 4e9e3779e1186279c549c1e4e200795b6354bd07 Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 18:09:05 -0700 Subject: [PATCH 169/177] add: initial draft --- .../project_wrapups/smrghsh_gsoc_2022.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 contributor_docs/project_wrapups/smrghsh_gsoc_2022.md diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md new file mode 100644 index 0000000000..69870c5d8a --- /dev/null +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -0,0 +1,66 @@ +# p5.xr Enter VR button, and controller API update + + +# Enter VR button -- Immersive Session Process Update +The first goal of my work with p5xr was to improve the process by which a user enters a VR app. I was inspired by the way Three.js VR projects offer a scene preview and a VR button. This offers significant improvements for use, for distribution, and for development. + +For the user - Immersive environments can be very intense experiences. By providing the user with a preview of the scene, a user can be primed with an expectation of experience, increasing comfort and safety. + +For a content distributor - Providing a 2D scene representation is akin to providing a thumbnail — this can be used to cue the user to enter or to help represent a set of content on a single interface. + +For the developer - By providing a scene preview, a developer can execute tests on the 3D environment without entering headset. This rapidly increases development time by giving a quicker view of the starting environment. This also allows for VR previews in a documentation page. + +![diagram of before and after](https://user-images.githubusercontent.com/22751315/179292673-63ed9c21-05af-448d-8a64-17f46f698206.png) + +This is how p5xr would display a VR scene has a green cube with a magenta background. Originally you could only see it once you click Enter VR and launch an immersive session. Now, you can preview the 3D scene. + +**View the original issue with plan and documentation** +[https://github.com/stalgiag/p5.xr/issues/169](https://github.com/stalgiag/p5.xr/issues/169) + +## Process Analysis +Working with WebXR Sessions and Devices was new to me, so under the advice of my mentor, I undertook a process analysis to learn about how three.js accomplished this goal and how p5js worked with WebXR. WebXR documentation feels quite sparse with regards to best practices and procedures, and it took me some time to learn. + +![Process diagram of Three.js](https://user-images.githubusercontent.com/22751315/179166072-0982e2b6-8fb3-4305-9727-c48f39cd6ad9.png) + +[Three.js’s implementation](https://github.com/mrdoob/three.js/blob/master/examples/jsm/webxr/VRButton.js) is quite simple, however, it does not provide for inline sessions— circumstances where VR/AR uses devices other than a head mounted display (HMD). + +![Process diagram of p5xr before PR](https://user-images.githubusercontent.com/22751315/179165850-8cf8d7b9-fcfc-433f-be3c-2b2d1afb517e.png) +p5xr accounted for this, but creating a process diagram revealed opportunities to streamline this process and load the environment earlier. I went through several iterations of streamlining the process, and eventually settled on a process design that hoisted device checks earlier in the process and reduced the amount of checks on the browser XR state. + +![Implemented process](https://user-images.githubusercontent.com/22751315/189506616-2aadef57-ade0-4e12-b34c-5aace04710e0.png) +**On figma, it’s easier to view the full process analysis diagrams:** +[Figma link](https://www.figma.com/file/MO8ffPGo90uwwua4qqyT4W/three.js-vs-p5xr-vs-new-p5xr-XR-launcher?node-id=0%3A1) + +### Challenges with implementation +One challenge that I struggled with was dealing with resetting the [reference space](https://immersive-web.github.io/webxr/spatial-tracking-explainer.html) in between launching a VR experience for the user and returning to an inline session in an HMD’s 2D web browser. This is needed because the physical space that a user navigates in an HMD is different than how they would interact in 2D. During this reset it’s necessary to set this, as well as a particular subset of `XR` properties to `null` in order to completely clear this, Examining a [similar issue with Aframe’s development](https://github.com/aframevr/aframe/issues/4406) helped in the solution. + +I also usually work with WebXR on a higher level, so I am really grateful for technical guidance through mentorship for working with WebXR at this lower level. This is invaluable information whose complexity was distilled for me throughout this process. It sparked many ideas for how we remove these technical barriers for VR development in arts and education. There is a complex history to WebXR devices as the specification is built for all the devices that are yet to come. +**View the pull request:** + +[https://github.com/stalgiag/p5.xr/pull/171](https://github.com/stalgiag/p5.xr/pull/171) + +# Controller API Update +![Video of Controller API update](https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e-b950-fecd9eb41ce2.mp4) + +- Including the [Quaternion.js](https://github.com/infusion/Quaternion.js/) library as a dependency. +- Exposing the rotation as Euler angles (while respecting the `angleMode`) for progress for [Controller Input Rotation Data #158](https://github.com/stalgiag/p5.xr/issues/158). There is some gimbal-locking type behavior that is persisting. +- Updating the manual input test to show controller tracking with orientation as well as primary and second button presses +- Added notice about gimbal locking to documentation + +**View the pull request** + +[https://github.com/stalgiag/p5.xr/pull/181](https://github.com/stalgiag/p5.xr/pull/181) + + +## Minor Contributions +- Small note to help Windows users with development, suggesting Git Bash to prevent errors with launching a local development server. Stalgia taught many git techniques and development norms. + +[https://github.com/stalgiag/p5.xr/pull/168](https://github.com/stalgiag/p5.xr/pull/168) + +- p5xr development has a set of manual tests in which the tester verifies app state manually; this pull request moves the objects in front of the tester, so they don’t have to turn their neck. I intended to increase usability and iteration speed with these changes + +[https://github.com/stalgiag/p5.xr/pull/170](https://github.com/stalgiag/p5.xr/pull/170) + + +# Acknowledgements +Under the mentorship of Stalgia Grigg, I have learned invaluable methods of development and technical communication. At present, WebXR adoption is still growing, and the lack of best practices make development complex for new open source contributors. Stalgia has a unique ability to elucidate complex processes, and has fostered my interest on how to remove technical barriers to VR development. \ No newline at end of file From 43a71fcc11acc2405ec1e6010ad7e7cbd158fdca Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 18:11:31 -0700 Subject: [PATCH 170/177] add: link to commits --- contributor_docs/project_wrapups/smrghsh_gsoc_2022.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md index 69870c5d8a..a06a169f11 100644 --- a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -1,4 +1,6 @@ -# p5.xr Enter VR button, and controller API update +# GSOC 22 Work Summary +To quickly view contributions please visit this link: +[https://github.com/stalgiag/p5.xr/commits?author=smrghsh](https://github.com/stalgiag/p5.xr/commits?author=smrghsh) # Enter VR button -- Immersive Session Process Update From 03faa823d49f47941eb8904b5c5efa5d93cbff98 Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 18:13:54 -0700 Subject: [PATCH 171/177] clean: consolidate acknowledgement --- contributor_docs/project_wrapups/smrghsh_gsoc_2022.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md index a06a169f11..c7dba8530c 100644 --- a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -36,9 +36,7 @@ p5xr accounted for this, but creating a process diagram revealed opportunities t ### Challenges with implementation One challenge that I struggled with was dealing with resetting the [reference space](https://immersive-web.github.io/webxr/spatial-tracking-explainer.html) in between launching a VR experience for the user and returning to an inline session in an HMD’s 2D web browser. This is needed because the physical space that a user navigates in an HMD is different than how they would interact in 2D. During this reset it’s necessary to set this, as well as a particular subset of `XR` properties to `null` in order to completely clear this, Examining a [similar issue with Aframe’s development](https://github.com/aframevr/aframe/issues/4406) helped in the solution. -I also usually work with WebXR on a higher level, so I am really grateful for technical guidance through mentorship for working with WebXR at this lower level. This is invaluable information whose complexity was distilled for me throughout this process. It sparked many ideas for how we remove these technical barriers for VR development in arts and education. There is a complex history to WebXR devices as the specification is built for all the devices that are yet to come. **View the pull request:** - [https://github.com/stalgiag/p5.xr/pull/171](https://github.com/stalgiag/p5.xr/pull/171) # Controller API Update From 2189006f0c7c77c073211d2607efeb33f12bbb24 Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 18:14:15 -0700 Subject: [PATCH 172/177] fix: use Github mp4 player for sample --- contributor_docs/project_wrapups/smrghsh_gsoc_2022.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md index c7dba8530c..888eb1b3d3 100644 --- a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -40,7 +40,7 @@ One challenge that I struggled with was dealing with resetting the [reference sp [https://github.com/stalgiag/p5.xr/pull/171](https://github.com/stalgiag/p5.xr/pull/171) # Controller API Update -![Video of Controller API update](https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e-b950-fecd9eb41ce2.mp4) +https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e-b950-fecd9eb41ce2.mp4 - Including the [Quaternion.js](https://github.com/infusion/Quaternion.js/) library as a dependency. - Exposing the rotation as Euler angles (while respecting the `angleMode`) for progress for [Controller Input Rotation Data #158](https://github.com/stalgiag/p5.xr/issues/158). There is some gimbal-locking type behavior that is persisting. From d95b209550daa29ac7091fdf91dd863ed96be076 Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 18:14:58 -0700 Subject: [PATCH 173/177] clean: remove trailing newline --- contributor_docs/project_wrapups/smrghsh_gsoc_2022.md | 1 - 1 file changed, 1 deletion(-) diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md index 888eb1b3d3..4153f5474b 100644 --- a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -48,7 +48,6 @@ https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e- - Added notice about gimbal locking to documentation **View the pull request** - [https://github.com/stalgiag/p5.xr/pull/181](https://github.com/stalgiag/p5.xr/pull/181) From 20e3371900648d541b6bbcee958b8606d22a5116 Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 18:18:05 -0700 Subject: [PATCH 174/177] clean: reword --- contributor_docs/project_wrapups/smrghsh_gsoc_2022.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md index 4153f5474b..bff642b573 100644 --- a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -52,7 +52,7 @@ https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e- ## Minor Contributions -- Small note to help Windows users with development, suggesting Git Bash to prevent errors with launching a local development server. Stalgia taught many git techniques and development norms. +- Small note to help Windows users with development, suggesting Git Bash to prevent errors with launching a local development server. Through this commit, Stalgia taught me contribution norms. [https://github.com/stalgiag/p5.xr/pull/168](https://github.com/stalgiag/p5.xr/pull/168) @@ -62,4 +62,5 @@ https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e- # Acknowledgements -Under the mentorship of Stalgia Grigg, I have learned invaluable methods of development and technical communication. At present, WebXR adoption is still growing, and the lack of best practices make development complex for new open source contributors. Stalgia has a unique ability to elucidate complex processes, and has fostered my interest on how to remove technical barriers to VR development. \ No newline at end of file +Under the mentorship of Stalgia Grigg, I have learned invaluable methods of development and technical communication. At present, WebXR adoption is still growing, and the lack of best practices make development complex for new open source contributors. Stalgia has a unique ability to elucidate complex processes, and has fostered my interest on how to remove technical barriers to VR development. + From 653576e578e38178532cc639d89470a8b425dd6a Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 18:32:08 -0700 Subject: [PATCH 175/177] add: acknowledgement --- contributor_docs/project_wrapups/smrghsh_gsoc_2022.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md index bff642b573..9802077dc8 100644 --- a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -62,5 +62,6 @@ https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e- # Acknowledgements -Under the mentorship of Stalgia Grigg, I have learned invaluable methods of development and technical communication. At present, WebXR adoption is still growing, and the lack of best practices make development complex for new open source contributors. Stalgia has a unique ability to elucidate complex processes, and has fostered my interest on how to remove technical barriers to VR development. +Under the mentorship of Stalgia Grigg, I have learned invaluable methods of development and technical communication. This is my first time contributing to open source, and my first time interfacing with the WebXR API. At present, WebXR adoption is still growing, and the lack of best practices make development complex for new open source contributors. Stalgia has a unique ability to elucidate complex processes, and has fostered my interest on how to remove technical barriers to VR development. p5.js has shaped me as an artist, a developer, and an educator-- it is the basis on which I have built my technical skill. The opportunity to engage with how this library approaches VR is an honor. Thank you for this mentorship. + From bf985c1249c8cf51fd65e8b18a5fd0b1e158b4a6 Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Sat, 10 Sep 2022 19:48:54 -0700 Subject: [PATCH 176/177] fix: change to acknowledgement --- contributor_docs/project_wrapups/smrghsh_gsoc_2022.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md index 9802077dc8..3a643297ac 100644 --- a/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md +++ b/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md @@ -62,6 +62,6 @@ https://user-images.githubusercontent.com/22751315/186528906-cf60441d-1cab-431e- # Acknowledgements -Under the mentorship of Stalgia Grigg, I have learned invaluable methods of development and technical communication. This is my first time contributing to open source, and my first time interfacing with the WebXR API. At present, WebXR adoption is still growing, and the lack of best practices make development complex for new open source contributors. Stalgia has a unique ability to elucidate complex processes, and has fostered my interest on how to remove technical barriers to VR development. p5.js has shaped me as an artist, a developer, and an educator-- it is the basis on which I have built my technical skill. The opportunity to engage with how this library approaches VR is an honor. Thank you for this mentorship. +Under the mentorship of Stalgia Grigg, I have learned invaluable methods of development and technical communication. This is my first time contributing to open source, and my first time interfacing with the WebXR API. At present, WebXR adoption is still growing, and the lack of best practices make development complex for new open source contributors. Stalgia has a unique ability to elucidate complex processes, and has fostered my interest on how to remove technical barriers to VR development. p5.js has shaped me as an artist, a developer, and an educator-- it is the basis on which I have built my technical skill. The opportunity to engage with how this library approaches VR is an honor. Thank you for this mentorship and opportunity. From fe8464702f43445bb5570f7199aa667d878f3045 Mon Sep 17 00:00:00 2001 From: Samir Ghosh Date: Mon, 12 Sep 2022 13:36:07 -0700 Subject: [PATCH 177/177] add: project_wrapup README link to smrghsh GSoC 22 --- contributor_docs/project_wrapups/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contributor_docs/project_wrapups/README.md b/contributor_docs/project_wrapups/README.md index 2cdcdd0731..9cde7d9d96 100644 --- a/contributor_docs/project_wrapups/README.md +++ b/contributor_docs/project_wrapups/README.md @@ -7,6 +7,7 @@ This folder contains wrap-up reports from p5.js related [Google Summer of Code]( ### Google Summer of Code 2022 * [p5 /teach reorganize & update](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/graciazhang_gsoc_2022.md) by Gracia Zhang, 2022 +* [p5xr Immersive Session Process and Controller API update](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/smrghsh_gsoc_2022.md) by Samir Ghosh, 2022 ### Google Summer of Code 2021 * [Adding Alt Text to the p5.js Website](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/katiejliu_gsoc_2021.md) by Katie Liu, 2021