/*! p5.js v2.2.1 February 11, 2026 */ var p5 = (function () { 'use strict'; /** * @module Constants * @submodule Constants * @for p5 */ const _PI = Math.PI; /** * Version of this p5.js. * @property {String} VERSION * @final */ const VERSION = '2.2.1'; // GRAPHICS RENDERER /** * The default, two-dimensional renderer in p5.js. * * Use this when calling (for example, * `createCanvas(400, 400, P2D)`) to specify a 2D context. * * @typedef {'p2d'} P2D * @property {P2D} P2D * @final */ const P2D = 'p2d'; /** * A high-dynamic-range (HDR) variant of the default, two-dimensional renderer. * * When available, this mode can allow for extended color ranges and more * dynamic color representation. Use it similarly to `P2D`: * `createCanvas(400, 400, P2DHDR)`. * * @typedef {'p2d-hdr'} P2DHDR * @property {P2DHDR} P2DHDR * @final */ const P2DHDR = 'p2d-hdr'; /** * One of the two render modes in p5.js, used for computationally intensive tasks like 3D rendering and shaders. * * `WEBGL` differs from the default `P2D` renderer in the following ways: * * - **Coordinate System** - When drawing in `WEBGL` mode, the origin point (0,0,0) is located at the center of the screen, not the top-left corner. See the tutorial page about coordinates and transformations. * - **3D Shapes** - `WEBGL` mode can be used to draw 3-dimensional shapes like box(), sphere(), cone(), and more. See the tutorial page about custom geometry to make more complex objects. * - **Shape Detail** - When drawing in `WEBGL` mode, you can specify how smooth curves should be drawn by using a `detail` parameter. See the wiki section about shapes for a more information and an example. * - **Textures** - A texture is like a skin that wraps onto a shape. See the wiki section about textures for examples of mapping images onto surfaces with textures. * - **Materials and Lighting** - `WEBGL` offers different types of lights like ambientLight() to place around a scene. Materials like specularMaterial() reflect the lighting to convey shape and depth. See the tutorial page for styling and appearance to experiment with different combinations. * - **Camera** - The viewport of a `WEBGL` sketch can be adjusted by changing camera attributes. See the tutorial page section about cameras for an explanation of camera controls. * - **Text** - `WEBGL` requires opentype/truetype font files to be preloaded using loadFont(). See the wiki section about text for details, along with a workaround. * - **Shaders** - Shaders are hardware accelerated programs that can be used for a variety of effects and graphics. See the introduction to shaders to get started with shaders in p5.js. * - **Graphics Acceleration** - `WEBGL` mode uses the graphics card instead of the CPU, so it may help boost the performance of your sketch (example: drawing more shapes on the screen at once). * * To learn more about WEBGL mode, check out all the interactive WEBGL tutorials in the "Tutorials" section of this website, or read the wiki article "Getting started with WebGL in p5". * * @typedef {'webgl'} WEBGL * @property {WEBGL} WEBGL * @final */ const WEBGL = 'webgl'; /** * One of the two possible values of a WebGL canvas (either WEBGL or WEBGL2), * which can be used to determine what capabilities the rendering environment * has. * @typedef {'webgl2'} WEBGL2 * @property {WEBGL2} WEBGL2 * @final */ const WEBGL2 = 'webgl2'; /** * A constant used for creating a WebGPU rendering context * @property {'webgpu'} WEBGPU * @final */ const WEBGPU = 'webgpu'; // ENVIRONMENT /** * @typedef {'default'} ARROW * @property {ARROW} ARROW * @final */ const ARROW = 'default'; /** * @property {String} SIMPLE * @final */ const SIMPLE = 'simple'; /** * @property {String} FULL * @final */ const FULL = 'full'; /** * @typedef {'crosshair'} CROSS * @property {CROSS} CROSS * @final */ const CROSS = 'crosshair'; /** * @typedef {'pointer'} HAND * @property {HAND} HAND * @final */ const HAND = 'pointer'; /** * @typedef {'move'} MOVE * @property {MOVE} MOVE * @final */ const MOVE = 'move'; /** * @typedef {'text'} TEXT * @property {TEXT} TEXT * @final */ const TEXT = 'text'; /** * @typedef {'wait'} WAIT * @property {WAIT} WAIT * @final */ const WAIT = 'wait'; // TRIGONOMETRY /** * A `Number` constant that's approximately 1.5708. * * `HALF_PI` is half the value of the mathematical constant π. It's useful for * many tasks that involve rotation and oscillation. For example, calling * `rotate(HALF_PI)` rotates the coordinate system `HALF_PI` radians, which is * a quarter turn (90˚). * * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI` * radians equals 90˚, and `QUARTER_PI` radians equals 45˚. * * @property {Number} HALF_PI * @final * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw an arc from 0 to HALF_PI. * arc(50, 50, 80, 80, 0, HALF_PI); * * describe('The bottom-right quarter of a circle drawn in white on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw a line. * line(0, 0, 40, 0); * * // Rotate a quarter turn. * rotate(HALF_PI); * * // Draw the same line, rotated. * line(0, 0, 40, 0); * * describe('Two black lines on a gray background. One line extends from the center to the right. The other line extends from the center to the bottom.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'A red circle and a blue circle oscillate from left to right on a gray background. The red circle appears to chase the blue circle.' * ); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Calculate the x-coordinates. * let x1 = 40 * sin(frameCount * 0.05); * let x2 = 40 * sin(frameCount * 0.05 + HALF_PI); * * // Style the oscillators. * noStroke(); * * // Draw the red oscillator. * fill(255, 0, 0); * circle(x1, 0, 20); * * // Draw the blue oscillator. * fill(0, 0, 255); * circle(x2, 0, 20); * } */ const HALF_PI = _PI / 2; /** * A `Number` constant that's approximately 3.1416. * * `PI` is the mathematical constant π. It's useful for many tasks that * involve rotation and oscillation. For example, calling `rotate(PI)` rotates * the coordinate system `PI` radians, which is a half turn (180˚). * * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI` * radians equals 90˚, and `QUARTER_PI` radians equals 45˚. * * @property {Number} PI * @final * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw an arc from 0 to PI. * arc(50, 50, 80, 80, 0, PI); * * describe('The bottom half of a circle drawn in white on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw a line. * line(0, 0, 40, 0); * * // Rotate a half turn. * rotate(PI); * * // Draw the same line, rotated. * line(0, 0, 40, 0); * * describe('A horizontal black line on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'A red circle and a blue circle oscillate from left to right on a gray background. The circles drift apart, then meet in the middle, over and over again.' * ); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Calculate the x-coordinates. * let x1 = 40 * sin(frameCount * 0.05); * let x2 = 40 * sin(frameCount * 0.05 + PI); * * // Style the oscillators. * noStroke(); * * // Draw the red oscillator. * fill(255, 0, 0); * circle(x1, 0, 20); * * // Draw the blue oscillator. * fill(0, 0, 255); * circle(x2, 0, 20); * } */ const PI = _PI; /** * A `Number` constant that's approximately 0.7854. * * `QUARTER_PI` is one-fourth the value of the mathematical constant π. It's * useful for many tasks that involve rotation and oscillation. For example, * calling `rotate(QUARTER_PI)` rotates the coordinate system `QUARTER_PI` * radians, which is an eighth of a turn (45˚). * * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI` * radians equals 90˚, and `QUARTER_PI` radians equals 45˚. * * @property {Number} QUARTER_PI * @final * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw an arc from 0 to QUARTER_PI. * arc(50, 50, 80, 80, 0, QUARTER_PI); * * describe('A one-eighth slice of a circle drawn in white on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw a line. * line(0, 0, 40, 0); * * // Rotate an eighth turn. * rotate(QUARTER_PI); * * // Draw the same line, rotated. * line(0, 0, 40, 0); * * describe('Two black lines that form a "V" opening towards the bottom-right corner of a gray square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'A red circle and a blue circle oscillate from left to right on a gray background. The red circle appears to chase the blue circle.' * ); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Calculate the x-coordinates. * let x1 = 40 * sin(frameCount * 0.05); * let x2 = 40 * sin(frameCount * 0.05 + QUARTER_PI); * * // Style the oscillators. * noStroke(); * * // Draw the red oscillator. * fill(255, 0, 0); * circle(x1, 0, 20); * * // Draw the blue oscillator. * fill(0, 0, 255); * circle(x2, 0, 20); * } */ const QUARTER_PI = _PI / 4; /** * A `Number` constant that's approximately 6.2382. * * `TAU` is twice the value of the mathematical constant π. It's useful for * many tasks that involve rotation and oscillation. For example, calling * `rotate(TAU)` rotates the coordinate system `TAU` radians, which is one * full turn (360˚). `TAU` and `TWO_PI` are equal. * * Note: `TAU` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI` * radians equals 90˚, and `QUARTER_PI` radians equals 45˚. * * @property {Number} TAU * @final * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw an arc from 0 to TAU. * arc(50, 50, 80, 80, 0, TAU); * * describe('A white circle drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw a line. * line(0, 0, 40, 0); * * // Rotate a full turn. * rotate(TAU); * * // Style the second line. * strokeWeight(5); * * // Draw the same line, shorter and rotated. * line(0, 0, 20, 0); * * describe( * 'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'A red circle with a blue center oscillates from left to right on a gray background.' * ); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Calculate the x-coordinates. * let x1 = 40 * sin(frameCount * 0.05); * let x2 = 40 * sin(frameCount * 0.05 + TAU); * * // Style the oscillators. * noStroke(); * * // Draw the red oscillator. * fill(255, 0, 0); * circle(x1, 0, 20); * * // Draw the blue oscillator, smaller. * fill(0, 0, 255); * circle(x2, 0, 10); * } */ const TAU = _PI * 2; /** * A `Number` constant that's approximately 6.2382. * * `TWO_PI` is twice the value of the mathematical constant π. It's useful for * many tasks that involve rotation and oscillation. For example, calling * `rotate(TWO_PI)` rotates the coordinate system `TWO_PI` radians, which is * one full turn (360˚). `TWO_PI` and `TAU` are equal. * * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI` * radians equals 90˚, and `QUARTER_PI` radians equals 45˚. * * @property {Number} TWO_PI * @final * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw an arc from 0 to TWO_PI. * arc(50, 50, 80, 80, 0, TWO_PI); * * describe('A white circle drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw a line. * line(0, 0, 40, 0); * * // Rotate a full turn. * rotate(TWO_PI); * * // Style the second line. * strokeWeight(5); * * // Draw the same line, shorter and rotated. * line(0, 0, 20, 0); * * describe( * 'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'A red circle with a blue center oscillates from left to right on a gray background.' * ); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Calculate the x-coordinates. * let x1 = 40 * sin(frameCount * 0.05); * let x2 = 40 * sin(frameCount * 0.05 + TWO_PI); * * // Style the oscillators. * noStroke(); * * // Draw the red oscillator. * fill(255, 0, 0); * circle(x1, 0, 20); * * // Draw the blue oscillator, smaller. * fill(0, 0, 255); * circle(x2, 0, 10); * } */ const TWO_PI = _PI * 2; /** * @property {Number} DEG_TO_RAD * @final */ const DEG_TO_RAD = _PI / 180.0; /** * @property {Number} RAD_TO_DEG * @final */ const RAD_TO_DEG = 180.0 / _PI; // SHAPE /** * @typedef {'corner'} CORNER * @property {CORNER} CORNER * @final */ const CORNER = 'corner'; /** * @typedef {'corners'} CORNERS * @property {CORNERS} CORNERS * @final */ const CORNERS = 'corners'; /** * @typedef {'radius'} RADIUS * @property {RADIUS} RADIUS * @final */ const RADIUS = 'radius'; /** * @typedef {'right'} RIGHT * @property {RIGHT} RIGHT * @final */ const RIGHT = 'right'; /** * @typedef {'left'} LEFT * @property {LEFT} LEFT * @final */ const LEFT = 'left'; /** * @typedef {'center'} CENTER * @property {CENTER} CENTER * @final */ const CENTER = 'center'; /** * @typedef {'top'} TOP * @property {TOP} TOP * @final */ const TOP = 'top'; /** * @typedef {'bottom'} BOTTOM * @property {BOTTOM} BOTTOM * @final */ const BOTTOM = 'bottom'; /** * @typedef {'alphabetic'} BASELINE * @property {BASELINE} BASELINE * @final */ const BASELINE = 'alphabetic'; /** * @typedef {0x0000} POINTS * @property {POINTS} POINTS * @final */ const POINTS = 0x0000; /** * @typedef {0x0001} LINES * @property {LINES} LINES * @final */ const LINES = 0x0001; /** * @typedef {0x0003} LINE_STRIP * @property {LINE_STRIP} LINE_STRIP * @final */ const LINE_STRIP = 0x0003; /** * @typedef {0x0002} LINE_LOOP * @property {LINE_LOOP} LINE_LOOP * @final */ const LINE_LOOP = 0x0002; /** * @typedef {0x0004} TRIANGLES * @property {TRIANGLES} TRIANGLES * @final */ const TRIANGLES = 0x0004; /** * @typedef {0x0006} TRIANGLE_FAN * @property {TRIANGLE_FAN} TRIANGLE_FAN * @final */ const TRIANGLE_FAN = 0x0006; /** * @typedef {0x0005} TRIANGLE_STRIP * @property {TRIANGLE_STRIP} TRIANGLE_STRIP * @final */ const TRIANGLE_STRIP = 0x0005; /** * @typedef {'quads'} QUADS * @property {QUADS} QUADS * @final */ const QUADS = 'quads'; /** * @typedef {'quad_strip'} QUAD_STRIP * @property {QUAD_STRIP} QUAD_STRIP * @final */ const QUAD_STRIP = 'quad_strip'; /** * @typedef {'tess'} TESS * @property {TESS} TESS * @final */ const TESS = 'tess'; /** * @typedef {0x0007} EMPTY_PATH * @property {EMPTY_PATH} EMPTY_PATH * @final */ const EMPTY_PATH = 0x0007; /** * @typedef {0x0008} PATH * @property {PATH} PATH * @final */ const PATH = 0x0008; /** * @typedef {'close'} CLOSE * @property {CLOSE} CLOSE * @final */ const CLOSE = 'close'; /** * @typedef {'open'} OPEN * @property {OPEN} OPEN * @final */ const OPEN = 'open'; /** * @typedef {'chord'} CHORD * @property {CHORD} CHORD * @final */ const CHORD = 'chord'; /** * @typedef {'pie'} PIE * @property {PIE} PIE * @final */ const PIE = 'pie'; /** * @typedef {'square'} PROJECT * @property {PROJECT} PROJECT * @final */ const PROJECT = 'square'; // PEND: careful this is counterintuitive /** * @typedef {'butt'} SQUARE * @property {SQUARE} SQUARE * @final */ const SQUARE = 'butt'; /** * @typedef {'round'} ROUND * @property {ROUND} ROUND * @final */ const ROUND = 'round'; /** * @typedef {'bevel'} BEVEL * @property {BEVEL} BEVEL * @final */ const BEVEL = 'bevel'; /** * @typedef {'miter'} MITER * @property {MITER} MITER * @final */ const MITER = 'miter'; // DOM EXTENSION /** * AUTO allows us to automatically set the width or height of an element (but not both), * based on the current height and width of the element. Only one parameter can * be passed to the size function as AUTO, at a time. * * @typedef {'auto'} AUTO * @property {AUTO} AUTO * @final */ const AUTO = 'auto'; // INPUT /** * @typedef {'Alt'} ALT * @property {ALT} ALT * @final */ const ALT = 'Alt'; /** * @typedef {'Backspace'} BACKSPACE * @property {BACKSPACE} BACKSPACE * @final */ const BACKSPACE = 'Backspace'; /** * @typedef {'Control' | 'Control'} CONTROL * @property {CONTROL} CONTROL * @final */ const CONTROL = 'Control'; /** * @typedef {'Delete'} DELETE * @property {DELETE} DELETE * @final */ const DELETE = 'Delete'; /** * @typedef {'ArrowDown'} DOWN_ARROW * @property {DOWN_ARROW} DOWN_ARROW * @final */ const DOWN_ARROW = 'ArrowDown'; /** * @typedef {'Enter'} ENTER * @property {ENTER} ENTER * @final */ const ENTER = 'Enter'; /** * @typedef {'Escape'} ESCAPE * @property {ESCAPE} ESCAPE * @final */ const ESCAPE = 'Escape'; /** * @typedef {'ArrowLeft'} LEFT_ARROW * @property {LEFT_ARROW} LEFT_ARROW * @final */ const LEFT_ARROW = 'ArrowLeft'; /** * @typedef {'Alt'} OPTION * @property {OPTION} OPTION * @final */ const OPTION = 'Alt'; /** * @typedef {'Enter'} RETURN * @property {RETURN} RETURN * @final */ const RETURN = 'Enter'; /** * @typedef {'ArrowRight'} RIGHT_ARROW * @property {RIGHT_ARROW} RIGHT_ARROW * @final */ const RIGHT_ARROW = 'ArrowRight'; /** * @typedef {'Shift'} SHIFT * @property {SHIFT} SHIFT * @final */ const SHIFT = 'Shift'; /** * @typedef {'Tab'} TAB * @property {TAB} TAB * @final */ const TAB = 'Tab'; /** * @typedef {'ArrowUp'} UP_ARROW * @property {UP_ARROW} UP_ARROW * @final */ const UP_ARROW = 'ArrowUp'; // RENDERING /** * @typedef {'source-over'} BLEND * @property {BLEND} BLEND * @final */ const BLEND = 'source-over'; /** * @typedef {'destination-out'} REMOVE * @property {REMOVE} REMOVE * @final */ const REMOVE = 'destination-out'; /** * @typedef {'lighter'} ADD * @property {ADD} ADD * @final */ const ADD = 'lighter'; /** * @typedef {'darken'} DARKEST * @property {DARKEST} DARKEST * @final */ const DARKEST = 'darken'; /** * @typedef {'lighten'} LIGHTEST * @property {LIGHTEST} LIGHTEST * @final */ const LIGHTEST = 'lighten'; /** * @typedef {'difference'} DIFFERENCE * @property {DIFFERENCE} DIFFERENCE * @final */ const DIFFERENCE = 'difference'; /** * @typedef {'subtract'} SUBTRACT * @property {SUBTRACT} SUBTRACT * @final */ const SUBTRACT = 'subtract'; /** * @typedef {'exclusion'} EXCLUSION * @property {EXCLUSION} EXCLUSION * @final */ const EXCLUSION = 'exclusion'; /** * @typedef {'multiply'} MULTIPLY * @property {MULTIPLY} MULTIPLY * @final */ const MULTIPLY = 'multiply'; /** * @typedef {'screen'} SCREEN * @property {SCREEN} SCREEN * @final */ const SCREEN = 'screen'; /** * @typedef {'copy'} REPLACE * @property {REPLACE} REPLACE * @final */ const REPLACE = 'copy'; /** * @typedef {'overlay'} OVERLAY * @property {OVERLAY} OVERLAY * @final */ const OVERLAY = 'overlay'; /** * @typedef {'hard-light'} HARD_LIGHT * @property {HARD_LIGHT} HARD_LIGHT * @final */ const HARD_LIGHT = 'hard-light'; /** * @typedef {'soft-light'} SOFT_LIGHT * @property {SOFT_LIGHT} SOFT_LIGHT * @final */ const SOFT_LIGHT = 'soft-light'; /** * @typedef {'color-dodge'} DODGE * @property {DODGE} DODGE * @final */ const DODGE = 'color-dodge'; /** * @typedef {'color-burn'} BURN * @property {BURN} BURN * @final */ const BURN = 'color-burn'; // FILTERS /** * @typedef {'threshold'} THRESHOLD * @property {THRESHOLD} THRESHOLD * @final */ const THRESHOLD = 'threshold'; /** * @typedef {'gray'} GRAY * @property {GRAY} GRAY * @final */ const GRAY = 'gray'; /** * @typedef {'opaque'} OPAQUE * @property {OPAQUE} OPAQUE * @final */ const OPAQUE = 'opaque'; /** * @typedef {'invert'} INVERT * @property {INVERT} INVERT * @final */ const INVERT = 'invert'; /** * @typedef {'posterize'} POSTERIZE * @property {POSTERIZE} POSTERIZE * @final */ const POSTERIZE = 'posterize'; /** * @typedef {'dilate'} DILATE * @property {DILATE} DILATE * @final */ const DILATE = 'dilate'; /** * @typedef {'erode'} ERODE * @property {ERODE} ERODE * @final */ const ERODE = 'erode'; /** * @typedef {'blur'} BLUR * @property {BLUR} BLUR * @final */ const BLUR = 'blur'; // TYPOGRAPHY /** * @typedef {'normal'} NORMAL * @property {NORMAL} NORMAL * @final */ const NORMAL = 'normal'; /** * @typedef {'italic'} ITALIC * @property {ITALIC} ITALIC * @final */ const ITALIC = 'italic'; /** * @typedef {'bold'} BOLD * @property {BOLD} BOLD * @final */ const BOLD = 'bold'; /** * @typedef {'bold italic'} BOLDITALIC * @property {BOLDITALIC} BOLDITALIC * @final */ const BOLDITALIC = 'bold italic'; /** * @typedef {'CHAR'} CHAR * @property {CHAR} CHAR * @final */ const CHAR = 'CHAR'; /** * @typedef {'WORD'} WORD * @property {WORD} WORD * @final */ const WORD = 'WORD'; // TYPOGRAPHY-INTERNAL const _DEFAULT_TEXT_FILL = '#000000'; const _DEFAULT_LEADMULT = 1.25; const _CTX_MIDDLE = 'middle'; // VERTICES /** * @typedef {'linear'} LINEAR * @property {LINEAR} LINEAR * @final */ const LINEAR = 'linear'; /** * @typedef {'quadratic'} QUADRATIC * @property {QUADRATIC} QUADRATIC * @final */ const QUADRATIC = 'quadratic'; /** * @typedef {'bezier'} BEZIER * @property {BEZIER} BEZIER * @final */ const BEZIER = 'bezier'; /** * @typedef {'curve'} CURVE * @property {CURVE} CURVE * @final */ const CURVE = 'curve'; // WEBGL DRAWMODES /** * @typedef {'stroke'} STROKE * @property {STROKE} STROKE * @final */ const STROKE = 'stroke'; /** * @typedef {'fill'} FILL * @property {FILL} FILL * @final */ const FILL = 'fill'; /** * @typedef {'texture'} TEXTURE * @property {TEXTURE} TEXTURE * @final */ const TEXTURE = 'texture'; /** * @typedef {'immediate'} IMMEDIATE * @property {IMMEDIATE} IMMEDIATE * @final */ const IMMEDIATE = 'immediate'; // WEBGL TEXTURE MODE // NORMAL already exists for typography /** * @typedef {'image'} IMAGE * @property {IMAGE} IMAGE * @final */ const IMAGE = 'image'; // WEBGL TEXTURE WRAP AND FILTERING // LINEAR already exists above /** * @typedef {'linear_mipmap'} LINEAR_MIPMAP * @property {LINEAR_MIPMAP} LINEAR_MIPMAP * @final * @private */ const LINEAR_MIPMAP = 'linear_mipmap'; /** * @typedef {'nearest'} NEAREST * @property {NEAREST} NEAREST * @final */ const NEAREST = 'nearest'; /** * @typedef {'repeat'} REPEAT * @property {REPEAT} REPEAT * @final */ const REPEAT = 'repeat'; /** * @typedef {'clamp'} CLAMP * @property {CLAMP} CLAMP * @final */ const CLAMP = 'clamp'; /** * @typedef {'mirror'} MIRROR * @property {MIRROR} MIRROR * @final */ const MIRROR = 'mirror'; // WEBGL GEOMETRY SHADING /** * @typedef {'flat'} FLAT * @property {FLAT} FLAT * @final */ const FLAT = 'flat'; /** * @typedef {'smooth'} SMOOTH * @property {SMOOTH} SMOOTH * @final */ const SMOOTH = 'smooth'; // DEVICE-ORIENTATION /** * @typedef {'landscape'} LANDSCAPE * @property {LANDSCAPE} LANDSCAPE * @final */ const LANDSCAPE = 'landscape'; /** * @typedef {'portrait'} PORTRAIT * @property {PORTRAIT} PORTRAIT * @final */ const PORTRAIT = 'portrait'; // DEFAULTS const _DEFAULT_STROKE = '#000000'; const _DEFAULT_FILL = '#FFFFFF'; /** * @typedef {'grid'} GRID * @property {GRID} GRID * @final */ const GRID = 'grid'; /** * @typedef {'axes'} AXES * @property {AXES} AXES * @final */ const AXES = 'axes'; /** * @typedef {'label'} LABEL * @property {LABEL} LABEL * @final */ const LABEL = 'label'; /** * @typedef {'fallback'} FALLBACK * @property {FALLBACK} FALLBACK * @final */ const FALLBACK = 'fallback'; /** * @typedef {'contain'} CONTAIN * @property {CONTAIN} CONTAIN * @final */ const CONTAIN = 'contain'; /** * @typedef {'cover'} COVER * @property {COVER} COVER * @final */ const COVER = 'cover'; /** * @typedef {'unsigned-byte'} UNSIGNED_BYTE * @property {UNSIGNED_BYTE} UNSIGNED_BYTE * @final */ const UNSIGNED_BYTE = 'unsigned-byte'; /** * @typedef {'unsigned-int'} UNSIGNED_INT * @property {UNSIGNED_INT} UNSIGNED_INT * @final */ const UNSIGNED_INT = 'unsigned-int'; /** * @typedef {'float'} FLOAT * @property {FLOAT} FLOAT * @final */ const FLOAT = 'float'; /** * @typedef {'half-float'} HALF_FLOAT * @property {HALF_FLOAT} HALF_FLOAT * @final */ const HALF_FLOAT = 'half-float'; /** * The `splineProperty('ends')` mode where splines curve through * their first and last points. * @typedef {unique symbol} INCLUDE * @property {INCLUDE} INCLUDE * @final */ const INCLUDE = Symbol('include'); /** * The `splineProperty('ends')` mode where the first and last points in a spline * affect the direction of the curve, but are not rendered. * @typedef {unique symbol} EXCLUDE * @property {EXCLUDE} EXCLUDE * @final */ const EXCLUDE = Symbol('exclude'); /** * The `splineProperty('ends')` mode where the spline loops back to its first point. * Only used internally. * @typedef {unique symbol} JOIN * @property {JOIN} JOIN * @final * @private */ const JOIN = Symbol('join'); var constants = /*#__PURE__*/Object.freeze({ __proto__: null, ADD: ADD, ALT: ALT, ARROW: ARROW, AUTO: AUTO, AXES: AXES, BACKSPACE: BACKSPACE, BASELINE: BASELINE, BEVEL: BEVEL, BEZIER: BEZIER, BLEND: BLEND, BLUR: BLUR, BOLD: BOLD, BOLDITALIC: BOLDITALIC, BOTTOM: BOTTOM, BURN: BURN, CENTER: CENTER, CHAR: CHAR, CHORD: CHORD, CLAMP: CLAMP, CLOSE: CLOSE, CONTAIN: CONTAIN, CONTROL: CONTROL, CORNER: CORNER, CORNERS: CORNERS, COVER: COVER, CROSS: CROSS, CURVE: CURVE, DARKEST: DARKEST, DEG_TO_RAD: DEG_TO_RAD, DELETE: DELETE, DIFFERENCE: DIFFERENCE, DILATE: DILATE, DODGE: DODGE, DOWN_ARROW: DOWN_ARROW, EMPTY_PATH: EMPTY_PATH, ENTER: ENTER, ERODE: ERODE, ESCAPE: ESCAPE, EXCLUDE: EXCLUDE, EXCLUSION: EXCLUSION, FALLBACK: FALLBACK, FILL: FILL, FLAT: FLAT, FLOAT: FLOAT, FULL: FULL, GRAY: GRAY, GRID: GRID, HALF_FLOAT: HALF_FLOAT, HALF_PI: HALF_PI, HAND: HAND, HARD_LIGHT: HARD_LIGHT, IMAGE: IMAGE, IMMEDIATE: IMMEDIATE, INCLUDE: INCLUDE, INVERT: INVERT, ITALIC: ITALIC, JOIN: JOIN, LABEL: LABEL, LANDSCAPE: LANDSCAPE, LEFT: LEFT, LEFT_ARROW: LEFT_ARROW, LIGHTEST: LIGHTEST, LINEAR: LINEAR, LINEAR_MIPMAP: LINEAR_MIPMAP, LINES: LINES, LINE_LOOP: LINE_LOOP, LINE_STRIP: LINE_STRIP, MIRROR: MIRROR, MITER: MITER, MOVE: MOVE, MULTIPLY: MULTIPLY, NEAREST: NEAREST, NORMAL: NORMAL, OPAQUE: OPAQUE, OPEN: OPEN, OPTION: OPTION, OVERLAY: OVERLAY, P2D: P2D, P2DHDR: P2DHDR, PATH: PATH, PI: PI, PIE: PIE, POINTS: POINTS, PORTRAIT: PORTRAIT, POSTERIZE: POSTERIZE, PROJECT: PROJECT, QUADRATIC: QUADRATIC, QUADS: QUADS, QUAD_STRIP: QUAD_STRIP, QUARTER_PI: QUARTER_PI, RADIUS: RADIUS, RAD_TO_DEG: RAD_TO_DEG, REMOVE: REMOVE, REPEAT: REPEAT, REPLACE: REPLACE, RETURN: RETURN, RIGHT: RIGHT, RIGHT_ARROW: RIGHT_ARROW, ROUND: ROUND, SCREEN: SCREEN, SHIFT: SHIFT, SIMPLE: SIMPLE, SMOOTH: SMOOTH, SOFT_LIGHT: SOFT_LIGHT, SQUARE: SQUARE, STROKE: STROKE, SUBTRACT: SUBTRACT, TAB: TAB, TAU: TAU, TESS: TESS, TEXT: TEXT, TEXTURE: TEXTURE, THRESHOLD: THRESHOLD, TOP: TOP, TRIANGLES: TRIANGLES, TRIANGLE_FAN: TRIANGLE_FAN, TRIANGLE_STRIP: TRIANGLE_STRIP, TWO_PI: TWO_PI, UNSIGNED_BYTE: UNSIGNED_BYTE, UNSIGNED_INT: UNSIGNED_INT, UP_ARROW: UP_ARROW, VERSION: VERSION, WAIT: WAIT, WEBGL: WEBGL, WEBGL2: WEBGL2, WEBGPU: WEBGPU, WORD: WORD, _CTX_MIDDLE: _CTX_MIDDLE, _DEFAULT_FILL: _DEFAULT_FILL, _DEFAULT_LEADMULT: _DEFAULT_LEADMULT, _DEFAULT_STROKE: _DEFAULT_STROKE, _DEFAULT_TEXT_FILL: _DEFAULT_TEXT_FILL }); /** * @module Transform * @submodule Transform * @for p5 * @requires core * @requires constants */ function transform$1(p5, fn){ /** * Applies a transformation matrix to the coordinate system. * * Transformations such as * translate(), * rotate(), and * scale() * use matrix-vector multiplication behind the scenes. A table of numbers, * called a matrix, encodes each transformation. The values in the matrix * then multiply each point on the canvas, which is represented by a vector. * * `applyMatrix()` allows for many transformations to be applied at once. See * Wikipedia * and MDN * for more details about transformations. * * There are two ways to call `applyMatrix()` in two and three dimensions. * * In 2D mode, the parameters `a`, `b`, `c`, `d`, `e`, and `f`, correspond to * elements in the following transformation matrix: * * > The transformation matrix used when applyMatrix is called in 2D mode. * * The numbers can be passed individually, as in * `applyMatrix(2, 0, 0, 0, 2, 0)`. They can also be passed in an array, as in * `applyMatrix([2, 0, 0, 0, 2, 0])`. * * In 3D mode, the parameters `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h`, `i`, * `j`, `k`, `l`, `m`, `n`, `o`, and `p` correspond to elements in the * following transformation matrix: * * The transformation matrix used when applyMatrix is called in 3D mode. * * The numbers can be passed individually, as in * `applyMatrix(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1)`. They can * also be passed in an array, as in * `applyMatrix([2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1])`. * * By default, transformations accumulate. The * push() and pop() functions * can be used to isolate transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `applyMatrix()` inside the draw() function won't * cause shapes to transform continuously. * * @method applyMatrix * @param {Array} arr an array containing the elements of the transformation matrix. Its length should be either 6 (2D) or 16 (3D). * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe('A white circle on a gray background.'); * } * * function draw() { * background(200); * * // Translate the origin to the center. * applyMatrix(1, 0, 0, 1, 50, 50); * * // Draw the circle at coordinates (0, 0). * circle(0, 0, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A white circle on a gray background.'); * } * * function draw() { * background(200); * * // Translate the origin to the center. * let m = [1, 0, 0, 1, 50, 50]; * applyMatrix(m); * * // Draw the circle at coordinates (0, 0). * circle(0, 0, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe("A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right."); * } * * function draw() { * background(200); * * // Rotate the coordinate system 1/8 turn. * let angle = QUARTER_PI; * let ca = cos(angle); * let sa = sin(angle); * applyMatrix(ca, sa, -sa, ca, 0, 0); * * // Draw a rectangle at coordinates (50, 0). * rect(50, 0, 40, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.' * ); * } * * function draw() { * background(200); * * // Draw a square at (30, 20). * square(30, 20, 40); * * // Scale the coordinate system by a factor of 0.5. * applyMatrix(0.5, 0, 0, 0.5, 0, 0); * * // Draw a square at (30, 20). * // It appears at (15, 10) after scaling. * square(30, 20, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A white quadrilateral on a gray background.'); * } * * function draw() { * background(200); * * // Calculate the shear factor. * let angle = QUARTER_PI; * let shearFactor = 1 / tan(HALF_PI - angle); * * // Shear the coordinate system along the x-axis. * applyMatrix(1, 0, shearFactor, 1, 0, 0); * * // Draw the square. * square(0, 0, 50); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube rotates slowly against a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system a little more each frame. * let angle = frameCount * 0.01; * let ca = cos(angle); * let sa = sin(angle); * applyMatrix(ca, 0, sa, 0, 0, 1, 0, 0, -sa, 0, ca, 0, 0, 0, 0, 1); * * // Draw a box. * box(); * } */ /** * @method applyMatrix * @param {Number} a an element of the transformation matrix. * @param {Number} b an element of the transformation matrix. * @param {Number} c an element of the transformation matrix. * @param {Number} d an element of the transformation matrix. * @param {Number} e an element of the transformation matrix. * @param {Number} f an element of the transformation matrix. * @chainable */ /** * @method applyMatrix * @param {Number} a * @param {Number} b * @param {Number} c * @param {Number} d * @param {Number} e * @param {Number} f * @param {Number} g an element of the transformation matrix. * @param {Number} h an element of the transformation matrix. * @param {Number} i an element of the transformation matrix. * @param {Number} j an element of the transformation matrix. * @param {Number} k an element of the transformation matrix. * @param {Number} l an element of the transformation matrix. * @param {Number} m an element of the transformation matrix. * @param {Number} n an element of the transformation matrix. * @param {Number} o an element of the transformation matrix. * @param {Number} p an element of the transformation matrix. * @chainable */ fn.applyMatrix = function(...args) { let isTypedArray = args[0] instanceof Object.getPrototypeOf(Uint8Array); if (Array.isArray(args[0]) || isTypedArray) { this._renderer.applyMatrix(...args[0]); } else { this._renderer.applyMatrix(...args); } return this; }; /** * Clears all transformations applied to the coordinate system. * * @method resetMatrix * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'Two circles drawn on a gray background. A blue circle is at the top-left and a red circle is at the bottom-right.' * ); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw a blue circle at the coordinates (25, 25). * fill('blue'); * circle(25, 25, 20); * * // Clear all transformations. * // The origin is now at the top-left corner. * resetMatrix(); * * // Draw a red circle at the coordinates (25, 25). * fill('red'); * circle(25, 25, 20); * } */ fn.resetMatrix = function() { this._renderer.resetMatrix(); return this; }; /** * Rotates the coordinate system. * * By default, the positive x-axis points to the right and the positive y-axis * points downward. The `rotate()` function changes this orientation by * rotating the coordinate system about the origin. Everything drawn after * `rotate()` is called will appear to be rotated. * * The first parameter, `angle`, is the amount to rotate. For example, calling * `rotate(1)` rotates the coordinate system clockwise 1 radian which is * nearly 57˚. `rotate()` interprets angle values using the current * angleMode(). * * The second parameter, `axis`, is optional. It's used to orient 3D rotations * in WebGL mode. If a p5.Vector is passed, as in * `rotate(QUARTER_PI, myVector)`, then the coordinate system will rotate * `QUARTER_PI` radians about `myVector`. If an array of vector components is * passed, as in `rotate(QUARTER_PI, [1, 0, 0])`, then the coordinate system * will rotate `QUARTER_PI` radians about a vector with the components * `[1, 0, 0]`. * * By default, transformations accumulate. For example, calling `rotate(1)` * twice has the same effect as calling `rotate(2)` once. The * push() and pop() functions * can be used to isolate transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `rotate(1)` inside the draw() function won't cause * shapes to spin. * * @method rotate * @param {Number} angle angle of rotation in the current angleMode(). * @param {p5.Vector|Number[]} [axis] axis to rotate about in 3D. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe( * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." * ); * } * * function draw() { * background(200); * * // Rotate the coordinate system 1/8 turn. * rotate(QUARTER_PI); * * // Draw a rectangle at coordinates (50, 0). * rect(50, 0, 40, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." * ); * } * * function draw() { * background(200); * * // Rotate the coordinate system 1/16 turn. * rotate(QUARTER_PI / 2); * * // Rotate the coordinate system another 1/16 turn. * rotate(QUARTER_PI / 2); * * // Draw a rectangle at coordinates (50, 0). * rect(50, 0, 40, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * // Use degrees. * angleMode(DEGREES); * * describe( * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." * ); * } * * function draw() { * background(200); * * // Rotate the coordinate system 1/8 turn. * rotate(45); * * // Draw a rectangle at coordinates (50, 0). * rect(50, 0, 40, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'A white rectangle on a gray background. The rectangle rotates slowly about the top-left corner. It disappears and reappears periodically.' * ); * } * * function draw() { * background(200); * * // Rotate the coordinate system a little more each frame. * let angle = frameCount * 0.01; * rotate(angle); * * // Draw a rectangle at coordinates (50, 0). * rect(50, 0, 40, 20); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe("A cube on a gray background. The cube's front face points to the top-right."); * } * * function draw() { * background(200); * * // Rotate the coordinate system 1/8 turn about * // the axis [1, 1, 0]. * let axis = createVector(1, 1, 0); * rotate(QUARTER_PI, axis); * * // Draw a box. * box(); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe("A cube on a gray background. The cube's front face points to the top-right."); * } * * function draw() { * background(200); * * // Rotate the coordinate system 1/8 turn about * // the axis [1, 1, 0]. * let axis = [1, 1, 0]; * rotate(QUARTER_PI, axis); * * // Draw a box. * box(); * } */ fn.rotate = function(angle, axis) { // p5._validateParameters('rotate', arguments); this._renderer.rotate(this._toRadians(angle), axis); return this; }; /** * Rotates the coordinate system about the x-axis in WebGL mode. * * The parameter, `angle`, is the amount to rotate. For example, calling * `rotateX(1)` rotates the coordinate system about the x-axis by 1 radian. * `rotateX()` interprets angle values using the current * angleMode(). * * By default, transformations accumulate. For example, calling `rotateX(1)` * twice has the same effect as calling `rotateX(2)` once. The * push() and pop() functions * can be used to isolate transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `rotateX(1)` inside the draw() function won't cause * shapes to spin. * * @method rotateX * @param {Number} angle angle of rotation in the current angleMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/8 turn. * rotateX(QUARTER_PI); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/16 turn. * rotateX(QUARTER_PI / 2); * * // Rotate the coordinate system 1/16 turn. * rotateX(QUARTER_PI / 2); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * // Use degrees. * angleMode(DEGREES); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/8 turn. * rotateX(45); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube rotates slowly against a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system a little more each frame. * let angle = frameCount * 0.01; * rotateX(angle); * * // Draw a box. * box(); * } */ fn.rotateX = function(angle) { this._assert3d('rotateX'); // p5._validateParameters('rotateX', arguments); this._renderer.rotateX(this._toRadians(angle)); return this; }; /** * Rotates the coordinate system about the y-axis in WebGL mode. * * The parameter, `angle`, is the amount to rotate. For example, calling * `rotateY(1)` rotates the coordinate system about the y-axis by 1 radian. * `rotateY()` interprets angle values using the current * angleMode(). * * By default, transformations accumulate. For example, calling `rotateY(1)` * twice has the same effect as calling `rotateY(2)` once. The * push() and pop() functions * can be used to isolate transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `rotateY(1)` inside the draw() function won't cause * shapes to spin. * * @method rotateY * @param {Number} angle angle of rotation in the current angleMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/8 turn. * rotateY(QUARTER_PI); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/16 turn. * rotateY(QUARTER_PI / 2); * * // Rotate the coordinate system 1/16 turn. * rotateY(QUARTER_PI / 2); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * // Use degrees. * angleMode(DEGREES); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/8 turn. * rotateY(45); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube rotates slowly against a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system a little more each frame. * let angle = frameCount * 0.01; * rotateY(angle); * * // Draw a box. * box(); * } */ fn.rotateY = function(angle) { this._assert3d('rotateY'); // p5._validateParameters('rotateY', arguments); this._renderer.rotateY(this._toRadians(angle)); return this; }; /** * Rotates the coordinate system about the z-axis in WebGL mode. * * The parameter, `angle`, is the amount to rotate. For example, calling * `rotateZ(1)` rotates the coordinate system about the z-axis by 1 radian. * `rotateZ()` interprets angle values using the current * angleMode(). * * By default, transformations accumulate. For example, calling `rotateZ(1)` * twice has the same effect as calling `rotateZ(2)` once. The * push() and pop() functions * can be used to isolate transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `rotateZ(1)` inside the draw() function won't cause * shapes to spin. * * @method rotateZ * @param {Number} angle angle of rotation in the current angleMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/8 turn. * rotateZ(QUARTER_PI); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/16 turn. * rotateZ(QUARTER_PI / 2); * * // Rotate the coordinate system 1/16 turn. * rotateZ(QUARTER_PI / 2); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * // Use degrees. * angleMode(DEGREES); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system 1/8 turn. * rotateZ(45); * * // Draw a box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube rotates slowly against a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Rotate the coordinate system a little more each frame. * let angle = frameCount * 0.01; * rotateZ(angle); * * // Draw a box. * box(); * } */ fn.rotateZ = function(angle) { this._assert3d('rotateZ'); // p5._validateParameters('rotateZ', arguments); this._renderer.rotateZ(this._toRadians(angle)); return this; }; /** * Scales the coordinate system. * * By default, shapes are drawn at their original scale. A rectangle that's 50 * pixels wide appears to take up half the width of a 100 pixel-wide canvas. * The `scale()` function can shrink or stretch the coordinate system so that * shapes appear at different sizes. There are two ways to call `scale()` with * parameters that set the scale factor(s). * * The first way to call `scale()` uses numbers to set the amount of scaling. * The first parameter, `s`, sets the amount to scale each axis. For example, * calling `scale(2)` stretches the x-, y-, and z-axes by a factor of 2. The * next two parameters, `y` and `z`, are optional. They set the amount to * scale the y- and z-axes. For example, calling `scale(2, 0.5, 1)` stretches * the x-axis by a factor of 2, shrinks the y-axis by a factor of 0.5, and * leaves the z-axis unchanged. * * The second way to call `scale()` uses a p5.Vector * object to set the scale factors. For example, calling `scale(myVector)` * uses the x-, y-, and z-components of `myVector` to set the amount of * scaling along the x-, y-, and z-axes. Doing so is the same as calling * `scale(myVector.x, myVector.y, myVector.z)`. * * By default, transformations accumulate. For example, calling `scale(1)` * twice has the same effect as calling `scale(2)` once. The * push() and pop() functions * can be used to isolate transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `scale(2)` inside the draw() function won't cause * shapes to grow continuously. * * @method scale * @param {Number|p5.Vector|Number[]} s amount to scale along the positive x-axis. * @param {Number} [y] amount to scale along the positive y-axis. Defaults to `s`. * @param {Number} [z] amount to scale along the positive z-axis. Defaults to `y`. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.' * ); * } * * function draw() { * background(200); * * // Draw a square at (30, 20). * square(30, 20, 40); * * // Scale the coordinate system by a factor of 0.5. * scale(0.5); * * // Draw a square at (30, 20). * // It appears at (15, 10) after scaling. * square(30, 20, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A rectangle and a square drawn in white on a gray background.'); * } * * function draw() { * background(200); * * // Draw a square at (30, 20). * square(30, 20, 40); * * // Scale the coordinate system by factors of * // 0.5 along the x-axis and * // 1.3 along the y-axis. * scale(0.5, 1.3); * * // Draw a square at (30, 20). * // It appears as a rectangle at (15, 26) after scaling. * square(30, 20, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A rectangle and a square drawn in white on a gray background.'); * } * * function draw() { * background(200); * * // Draw a square at (30, 20). * square(30, 20, 40); * * // Create a p5.Vector object. * let v = createVector(0.5, 1.3); * * // Scale the coordinate system by factors of * // 0.5 along the x-axis and * // 1.3 along the y-axis. * scale(v); * * // Draw a square at (30, 20). * // It appears as a rectangle at (15, 26) after scaling. * square(30, 20, 40); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A red box and a blue box drawn on a gray background. The red box appears embedded in the blue box.' * ); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the spheres. * noStroke(); * * // Draw the red sphere. * fill('red'); * box(); * * // Scale the coordinate system by factors of * // 0.5 along the x-axis and * // 1.3 along the y-axis and * // 2 along the z-axis. * scale(0.5, 1.3, 2); * * // Draw the blue sphere. * fill('blue'); * box(); * } */ /** * @method scale * @param {p5.Vector|Number[]} scales vector whose components should be used to scale. * @chainable */ fn.scale = function(x, y, z) { // p5._validateParameters('scale', arguments); // Only check for Vector argument type if Vector is available if (x instanceof p5.Vector) { const v = x; x = v.x; y = v.y; z = v.z; } else if (Array.isArray(x)) { const rg = x; x = rg[0]; y = rg[1]; z = rg[2] || 1; } if (isNaN(y)) { y = z = x; } else if (isNaN(z)) { z = 1; } this._renderer.scale(x, y, z); return this; }; /** * Shears the x-axis so that shapes appear skewed. * * By default, the x- and y-axes are perpendicular. The `shearX()` function * transforms the coordinate system so that x-coordinates are translated while * y-coordinates are fixed. * * The first parameter, `angle`, is the amount to shear. For example, calling * `shearX(1)` transforms all x-coordinates using the formula * `x = x + y * tan(angle)`. `shearX()` interprets angle values using the * current angleMode(). * * By default, transformations accumulate. For example, calling * `shearX(1)` twice has the same effect as calling `shearX(2)` once. The * push() and * pop() functions can be used to isolate * transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `shearX(1)` inside the draw() function won't * cause shapes to shear continuously. * * @method shearX * @param {Number} angle angle to shear by in the current angleMode(). * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe('A white quadrilateral on a gray background.'); * } * * function draw() { * background(200); * * // Shear the coordinate system along the x-axis. * shearX(QUARTER_PI); * * // Draw the square. * square(0, 0, 50); * } * * @example * function setup() { * createCanvas(100, 100); * * // Use degrees. * angleMode(DEGREES); * * describe('A white quadrilateral on a gray background.'); * } * * function draw() { * background(200); * * // Shear the coordinate system along the x-axis. * shearX(45); * * // Draw the square. * square(0, 0, 50); * } */ fn.shearX = function(angle) { // p5._validateParameters('shearX', arguments); const rad = this._toRadians(angle); this._renderer.applyMatrix(1, 0, Math.tan(rad), 1, 0, 0); return this; }; /** * Shears the y-axis so that shapes appear skewed. * * By default, the x- and y-axes are perpendicular. The `shearY()` function * transforms the coordinate system so that y-coordinates are translated while * x-coordinates are fixed. * * The first parameter, `angle`, is the amount to shear. For example, calling * `shearY(1)` transforms all y-coordinates using the formula * `y = y + x * tan(angle)`. `shearY()` interprets angle values using the * current angleMode(). * * By default, transformations accumulate. For example, calling * `shearY(1)` twice has the same effect as calling `shearY(2)` once. The * push() and * pop() functions can be used to isolate * transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `shearY(1)` inside the draw() function won't * cause shapes to shear continuously. * * @method shearY * @param {Number} angle angle to shear by in the current angleMode(). * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe('A white quadrilateral on a gray background.'); * } * * function draw() { * background(200); * * // Shear the coordinate system along the y-axis. * shearY(QUARTER_PI); * * // Draw the square. * square(0, 0, 50); * } * * @example * function setup() { * createCanvas(100, 100); * * // Use degrees. * angleMode(DEGREES); * * describe('A white quadrilateral on a gray background.'); * } * * function draw() { * background(200); * * // Shear the coordinate system along the y-axis. * shearY(45); * * // Draw the square. * square(0, 0, 50); * } */ fn.shearY = function(angle) { // p5._validateParameters('shearY', arguments); const rad = this._toRadians(angle); this._renderer.applyMatrix(1, Math.tan(rad), 0, 1, 0, 0); return this; }; /** * Translates the coordinate system. * * By default, the origin `(0, 0)` is at the sketch's top-left corner in 2D * mode and center in WebGL mode. The `translate()` function shifts the origin * to a different position. Everything drawn after `translate()` is called * will appear to be shifted. There are two ways to call `translate()` with * parameters that set the origin's position. * * The first way to call `translate()` uses numbers to set the amount of * translation. The first two parameters, `x` and `y`, set the amount to * translate along the positive x- and y-axes. For example, calling * `translate(20, 30)` translates the origin 20 pixels along the x-axis and 30 * pixels along the y-axis. The third parameter, `z`, is optional. It sets the * amount to translate along the positive z-axis. For example, calling * `translate(20, 30, 40)` translates the origin 20 pixels along the x-axis, * 30 pixels along the y-axis, and 40 pixels along the z-axis. * * The second way to call `translate()` uses a * p5.Vector object to set the amount of * translation. For example, calling `translate(myVector)` uses the x-, y-, * and z-components of `myVector` to set the amount to translate along the x-, * y-, and z-axes. Doing so is the same as calling * `translate(myVector.x, myVector.y, myVector.z)`. * * By default, transformations accumulate. For example, calling * `translate(10, 0)` twice has the same effect as calling * `translate(20, 0)` once. The push() and * pop() functions can be used to isolate * transformations within distinct drawing groups. * * Note: Transformations are reset at the beginning of the draw loop. Calling * `translate(10, 0)` inside the draw() function won't * cause shapes to move continuously. * * @method translate * @param {Number} x amount to translate along the positive x-axis. * @param {Number} y amount to translate along the positive y-axis. * @param {Number} [z] amount to translate along the positive z-axis. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe('A white circle on a gray background.'); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw a circle at coordinates (0, 0). * circle(0, 0, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'Two circles drawn on a gray background. The blue circle on the right overlaps the red circle at the center.' * ); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Draw the red circle. * fill('red'); * circle(0, 0, 40); * * // Translate the origin to the right. * translate(25, 0); * * // Draw the blue circle. * fill('blue'); * circle(0, 0, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A white circle moves slowly from left to right on a gray background.'); * } * * function draw() { * background(200); * * // Calculate the x-coordinate. * let x = frameCount * 0.2; * * // Translate the origin. * translate(x, 50); * * // Draw a circle at coordinates (0, 0). * circle(0, 0, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A white circle on a gray background.'); * } * * function draw() { * background(200); * * // Create a p5.Vector object. * let v = createVector(50, 50); * * // Translate the origin by the vector. * translate(v); * * // Draw a circle at coordinates (0, 0). * circle(0, 0, 40); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'Two spheres sitting side-by-side on gray background. The sphere at the center is red. The sphere on the right is blue.' * ); * } * * function draw() { * background(200); * * // Turn on the lights. * lights(); * * // Style the spheres. * noStroke(); * * // Draw the red sphere. * fill('red'); * sphere(10); * * // Translate the origin to the right. * translate(30, 0, 0); * * // Draw the blue sphere. * fill('blue'); * sphere(10); * } */ /** * @method translate * @param {p5.Vector} vector vector by which to translate. * @chainable */ fn.translate = function(x, y, z) { // p5._validateParameters('translate', arguments); if (this._renderer.isP3D) { this._renderer.translate(x, y, z); } else { this._renderer.translate(x, y); } return this; }; /** * Begins a drawing group that contains its own styles and transformations. * * By default, styles such as fill() and * transformations such as rotate() are applied to * all drawing that follows. The `push()` and pop() * functions can limit the effect of styles and transformations to a specific * group of shapes, images, and text. For example, a group of shapes could be * translated to follow the mouse without affecting the rest of the sketch: * * ```js * // Begin the drawing group. * push(); * * // Translate the origin to the mouse's position. * translate(mouseX, mouseY); * * // Style the face. * noStroke(); * fill('green'); * * // Draw the face. * circle(0, 0, 60); * * // Style the eyes. * fill('white'); * * // Draw the left eye. * ellipse(-20, -20, 30, 20); * * // Draw the right eye. * ellipse(20, -20, 30, 20); * * // End the drawing group. * pop(); * * // Draw a bug. * let x = random(0, 100); * let y = random(0, 100); * text('🦟', x, y); * ``` * * In the code snippet above, the bug's position isn't affected by * `translate(mouseX, mouseY)` because that transformation is contained * between `push()` and pop(). The bug moves around * the entire canvas as expected. * * Note: `push()` and pop() are always called as a * pair. Both functions are required to begin and end a drawing group. * * `push()` and pop() can also be nested to create * subgroups. For example, the code snippet above could be changed to give * more detail to the frog’s eyes: * * ```js * // Begin the drawing group. * push(); * * // Translate the origin to the mouse's position. * translate(mouseX, mouseY); * * // Style the face. * noStroke(); * fill('green'); * * // Draw a face. * circle(0, 0, 60); * * // Style the eyes. * fill('white'); * * // Draw the left eye. * push(); * translate(-20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // Draw the right eye. * push(); * translate(20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // End the drawing group. * pop(); * * // Draw a bug. * let x = random(0, 100); * let y = random(0, 100); * text('🦟', x, y); * ``` * * In this version, the code to draw each eye is contained between its own * `push()` and pop() functions. Doing so makes it * easier to add details in the correct part of a drawing. * * `push()` and pop() contain the effects of the * following functions: * * - fill() * - noFill() * - noStroke() * - stroke() * - tint() * - noTint() * - strokeWeight() * - strokeCap() * - strokeJoin() * - imageMode() * - rectMode() * - ellipseMode() * - colorMode() * - textAlign() * - textFont() * - textSize() * - textLeading() * - applyMatrix() * - resetMatrix() * - rotate() * - scale() * - shearX() * - shearY() * - translate() * * In WebGL mode, `push()` and pop() contain the * effects of a few additional styles: * * - setCamera() * - ambientLight() * - directionalLight() * - pointLight() texture() * - specularMaterial() * - shininess() * - normalMaterial() * - shader() * * @method push * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the left circle. * circle(25, 50, 20); * * // Begin the drawing group. * push(); * * // Translate the origin to the center. * translate(50, 50); * * // Style the circle. * strokeWeight(5); * stroke('royalblue'); * fill('orange'); * * // Draw the circle. * circle(0, 0, 20); * * // End the drawing group. * pop(); * * // Draw the right circle. * circle(75, 50, 20); * * describe( * 'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * // Slow the frame rate. * frameRate(24); * * describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.'); * } * * function draw() { * background(200); * * // Begin the drawing group. * push(); * * // Translate the origin to the mouse's position. * translate(mouseX, mouseY); * * // Style the face. * noStroke(); * fill('green'); * * // Draw a face. * circle(0, 0, 60); * * // Style the eyes. * fill('white'); * * // Draw the left eye. * push(); * translate(-20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // Draw the right eye. * push(); * translate(20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // End the drawing group. * pop(); * * // Draw a bug. * let x = random(0, 100); * let y = random(0, 100); * text('🦟', x, y); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.' * ); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the red sphere. * push(); * translate(-25, 0, 0); * noStroke(); * directionalLight(255, 0, 0, 0, 0, -1); * sphere(20); * pop(); * * // Draw the blue sphere. * push(); * translate(25, 0, 0); * strokeWeight(0.3); * stroke(0, 0, 255); * noFill(); * sphere(20); * pop(); * } */ fn.push = function() { this._renderer.push(); }; /** * Ends a drawing group that contains its own styles and transformations. * * By default, styles such as fill() and * transformations such as rotate() are applied to * all drawing that follows. The push() and `pop()` * functions can limit the effect of styles and transformations to a specific * group of shapes, images, and text. For example, a group of shapes could be * translated to follow the mouse without affecting the rest of the sketch: * * ```js * // Begin the drawing group. * push(); * * // Translate the origin to the mouse's position. * translate(mouseX, mouseY); * * // Style the face. * noStroke(); * fill('green'); * * // Draw the face. * circle(0, 0, 60); * * // Style the eyes. * fill('white'); * * // Draw the left eye. * ellipse(-20, -20, 30, 20); * * // Draw the right eye. * ellipse(20, -20, 30, 20); * * // End the drawing group. * pop(); * * // Draw a bug. * let x = random(0, 100); * let y = random(0, 100); * text('🦟', x, y); * ``` * * In the code snippet above, the bug's position isn't affected by * `translate(mouseX, mouseY)` because that transformation is contained * between push() and `pop()`. The bug moves around * the entire canvas as expected. * * Note: push() and `pop()` are always called as a * pair. Both functions are required to begin and end a drawing group. * * push() and `pop()` can also be nested to create * subgroups. For example, the code snippet above could be changed to give * more detail to the frog’s eyes: * * ```js * // Begin the drawing group. * push(); * * // Translate the origin to the mouse's position. * translate(mouseX, mouseY); * * // Style the face. * noStroke(); * fill('green'); * * // Draw a face. * circle(0, 0, 60); * * // Style the eyes. * fill('white'); * * // Draw the left eye. * push(); * translate(-20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // Draw the right eye. * push(); * translate(20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // End the drawing group. * pop(); * * // Draw a bug. * let x = random(0, 100); * let y = random(0, 100); * text('🦟', x, y); * ``` * * In this version, the code to draw each eye is contained between its own * push() and `pop()` functions. Doing so makes it * easier to add details in the correct part of a drawing. * * push() and `pop()` contain the effects of the * following functions: * * - fill() * - noFill() * - noStroke() * - stroke() * - tint() * - noTint() * - strokeWeight() * - strokeCap() * - strokeJoin() * - imageMode() * - rectMode() * - ellipseMode() * - colorMode() * - textAlign() * - textFont() * - textSize() * - textLeading() * - applyMatrix() * - resetMatrix() * - rotate() * - scale() * - shearX() * - shearY() * - translate() * * In WebGL mode, push() and `pop()` contain the * effects of a few additional styles: * * - setCamera() * - ambientLight() * - directionalLight() * - pointLight() texture() * - specularMaterial() * - shininess() * - normalMaterial() * - shader() * * @method pop * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the left circle. * circle(25, 50, 20); * * // Begin the drawing group. * push(); * * // Translate the origin to the center. * translate(50, 50); * * // Style the circle. * strokeWeight(5); * stroke('royalblue'); * fill('orange'); * * // Draw the circle. * circle(0, 0, 20); * * // End the drawing group. * pop(); * * // Draw the right circle. * circle(75, 50, 20); * * describe( * 'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * // Slow the frame rate. * frameRate(24); * * describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.'); * } * * function draw() { * background(200); * * // Begin the drawing group. * push(); * * // Translate the origin to the mouse's position. * translate(mouseX, mouseY); * * // Style the face. * noStroke(); * fill('green'); * * // Draw a face. * circle(0, 0, 60); * * // Style the eyes. * fill('white'); * * // Draw the left eye. * push(); * translate(-20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // Draw the right eye. * push(); * translate(20, -20); * ellipse(0, 0, 30, 20); * fill('black'); * circle(0, 0, 8); * pop(); * * // End the drawing group. * pop(); * * // Draw a bug. * let x = random(0, 100); * let y = random(0, 100); * text('🦟', x, y); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.' * ); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the red sphere. * push(); * translate(-25, 0, 0); * noStroke(); * directionalLight(255, 0, 0, 0, 0, -1); * sphere(20); * pop(); * * // Draw the blue sphere. * push(); * translate(25, 0, 0); * strokeWeight(0.3); * stroke(0, 0, 255); * noFill(); * sphere(20); * pop(); * } */ fn.pop = function() { this._renderer.pop(); }; } if(typeof p5 !== 'undefined'){ transform$1(p5, p5.prototype); } /** * @module Structure * @submodule Structure * @for p5 * @requires core */ function structure(p5, fn){ /** * Stops the code in draw() from running repeatedly. * * By default, draw() tries to run 60 times per * second. Calling `noLoop()` stops draw() from * repeating. The draw loop can be restarted by calling * loop(). draw() can be run * once by calling redraw(). * * The isLooping() function can be used to check * whether a sketch is looping, as in `isLooping() === true`. * * @method noLoop * * @example * function setup() { * createCanvas(100, 100); * * // Turn off the draw loop. * noLoop(); * * describe('A white half-circle on the left edge of a gray square.'); * } * * function draw() { * background(200); * * // Calculate the circle's x-coordinate. * let x = frameCount; * * // Draw the circle. * // Normally, the circle would move from left to right. * circle(x, 50, 20); * } * * @example * // Double-click to stop the draw loop. * * function setup() { * createCanvas(100, 100); * * // Slow the frame rate. * frameRate(5); * * describe('A white circle moves randomly on a gray background. It stops moving when the user double-clicks.'); * } * * function draw() { * background(200); * * // Calculate the circle's coordinates. * let x = random(0, 100); * let y = random(0, 100); * * // Draw the circle. * // Normally, the circle would move from left to right. * circle(x, y, 20); * } * * // Stop the draw loop when the user double-clicks. * function doubleClicked() { * noLoop(); * } * * @example * let startButton; * let stopButton; * * function setup() { * createCanvas(100, 100); * * // Create the button elements and place them * // beneath the canvas. * startButton = createButton('▶'); * startButton.position(0, 100); * startButton.size(50, 20); * stopButton = createButton('◾'); * stopButton.position(50, 100); * stopButton.size(50, 20); * * // Set functions to call when the buttons are pressed. * startButton.mousePressed(loop); * stopButton.mousePressed(noLoop); * * // Slow the frame rate. * frameRate(5); * * describe( * 'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.' * ); * } * * function draw() { * background(200); * * // Calculate the circle's coordinates. * let x = random(0, 100); * let y = random(0, 100); * * // Draw the circle. * // Normally, the circle would move from left to right. * circle(x, y, 20); * } */ fn.noLoop = function() { this._loop = false; }; /** * Resumes the draw loop after noLoop() has been * called. * * By default, draw() tries to run 60 times per * second. Calling noLoop() stops * draw() from repeating. The draw loop can be * restarted by calling `loop()`. * * The isLooping() function can be used to check * whether a sketch is looping, as in `isLooping() === true`. * * @method loop * * @example * function setup() { * createCanvas(100, 100); * * // Turn off the draw loop. * noLoop(); * * describe( * 'A white half-circle on the left edge of a gray square. The circle starts moving to the right when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Calculate the circle's x-coordinate. * let x = frameCount; * * // Draw the circle. * circle(x, 50, 20); * } * * // Resume the draw loop when the user double-clicks. * function doubleClicked() { * loop(); * } * * @example * let startButton; * let stopButton; * * function setup() { * createCanvas(100, 100); * * // Create the button elements and place them * // beneath the canvas. * startButton = createButton('▶'); * startButton.position(0, 100); * startButton.size(50, 20); * stopButton = createButton('◾'); * stopButton.position(50, 100); * stopButton.size(50, 20); * * // Set functions to call when the buttons are pressed. * startButton.mousePressed(loop); * stopButton.mousePressed(noLoop); * * // Slow the frame rate. * frameRate(5); * * describe( * 'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.' * ); * } * * function draw() { * background(200); * * // Calculate the circle's coordinates. * let x = random(0, 100); * let y = random(0, 100); * * // Draw the circle. * // Normally, the circle would move from left to right. * circle(x, y, 20); * } */ fn.loop = function() { if (!this._loop) { this._loop = true; if (this._setupDone) { this._draw(); } } }; /** * Returns `true` if the draw loop is running and `false` if not. * * By default, draw() tries to run 60 times per * second. Calling noLoop() stops * draw() from repeating. The draw loop can be * restarted by calling loop(). * * The `isLooping()` function can be used to check whether a sketch is * looping, as in `isLooping() === true`. * * @method isLooping * @returns {boolean} * * @example * function setup() { * createCanvas(100, 100); * * describe('A white circle drawn against a gray background. When the user double-clicks, the circle stops or resumes following the mouse.'); * } * * function draw() { * background(200); * * // Draw the circle at the mouse's position. * circle(mouseX, mouseY, 20); * } * * // Toggle the draw loop when the user double-clicks. * function doubleClicked() { * if (isLooping() === true) { * noLoop(); * } else { * loop(); * } * } */ fn.isLooping = function() { return this._loop; }; /** * Runs the code in draw() once. * * By default, draw() tries to run 60 times per * second. Calling noLoop() stops * draw() from repeating. Calling `redraw()` will * execute the code in the draw() function a set * number of times. `await` the result of `redraw` to make sure it has finished. * * The parameter, `n`, is optional. If a number is passed, as in `redraw(5)`, * then the draw loop will run the given number of times. By default, `n` is * 1. * * @method redraw * @param {Integer} [n] number of times to run draw(). Defaults to 1. * @returns {Promise} * * @example * // Double-click the canvas to move the circle. * * let x = 0; * * function setup() { * createCanvas(100, 100); * * // Turn off the draw loop. * noLoop(); * * describe( * 'A white half-circle on the left edge of a gray square. The circle moves a little to the right when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Draw the circle. * circle(x, 50, 20); * * // Increment x. * x += 5; * } * * // Run the draw loop when the user double-clicks. * async function doubleClicked() { * await redraw(); * } * * @example * // Double-click the canvas to move the circle. * * let x = 0; * * function setup() { * createCanvas(100, 100); * * // Turn off the draw loop. * noLoop(); * * describe( * 'A white half-circle on the left edge of a gray square. The circle hops to the right when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Draw the circle. * circle(x, 50, 20); * * // Increment x. * x += 5; * } * * // Run the draw loop three times when the user double-clicks. * async function doubleClicked() { * await redraw(3); * } */ fn.redraw = async function(n) { if (this._inUserDraw || !this._setupDone) { return; } let numberOfRedraws = parseInt(n); if (isNaN(numberOfRedraws) || numberOfRedraws < 1) { numberOfRedraws = 1; } const context = this._isGlobal ? window : this; if (typeof context.draw === 'function') { if (typeof context.setup === 'undefined') { context.scale(context._pixelDensity, context._pixelDensity); } for (let idxRedraw = 0; idxRedraw < numberOfRedraws; idxRedraw++) { context.resetMatrix(); if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._updateAccsOutput(); } if (this._renderer.isP3D) { this._renderer._update(); } this.frameCount = context.frameCount + 1; await this._runLifecycleHook('predraw'); this._inUserDraw = true; try { await context.draw(); } finally { this._inUserDraw = false; } await this._runLifecycleHook('postdraw'); } // Finish drawing await this._renderer.finishDraw?.(); } }; /** * Creates a new sketch in "instance" mode. * * All p5.js sketches are instances of the `p5` class. Put another way, all * p5.js sketches are objects with methods including `pInst.setup()`, * `pInst.draw()`, `pInst.circle()`, and `pInst.fill()`. By default, sketches * run in "global mode" to hide some of this complexity. * * In global mode, a default instance of the `p5` class is created * automatically. The default `p5` instance searches the web page's source * code for declarations of system functions such as `setup()`, `draw()`, * and `mousePressed()`, then attaches those functions to itself as methods. * Calling a function such as `circle()` in global mode actually calls the * default `p5` object's `pInst.circle()` method. * * It's often helpful to isolate the code within sketches from the rest of the * code on a web page. Two common use cases are web pages that use other * JavaScript libraries and web pages with multiple sketches. "Instance mode" * makes it easy to support both of these scenarios. * * Instance mode sketches support the same API as global mode sketches. They * use a function to bundle, or encapsulate, an entire sketch. The function * containing the sketch is then passed to the `p5()` constructor. * * The first parameter, `sketch`, is a function that contains the sketch. For * example, the statement `new p5(mySketch)` would create a new instance mode * sketch from a function named `mySketch`. The function should have one * parameter, `p`, that's a `p5` object. * * The second parameter, `node`, is optional. If a string is passed, as in * `new p5(mySketch, 'sketch-one')` the new instance mode sketch will become a * child of the HTML element with the id `sketch-one`. If an HTML element is * passed, as in `new p5(mySketch, myElement)`, then the new instance mode * sketch will become a child of the `Element` object called `myElement`. * * @method p5 * @param {Object} sketch function containing the sketch. * @param {String|HTMLElement} node ID or reference to the HTML element that will contain the sketch. * * @example * // META:norender * // Declare the function containing the sketch. * function sketch(p) { * * // Declare the setup() method. * p.setup = function () { * p.createCanvas(100, 100); * * p.describe('A white circle drawn on a gray background.'); * }; * * // Declare the draw() method. * p.draw = function () { * p.background(200); * * // Draw the circle. * p.circle(50, 50, 20); * }; * } * * // Initialize the sketch. * new p5(sketch); * * @example * // META:norender * // Declare the function containing the sketch. * function sketch(p) { * // Create the sketch's variables within its scope. * let x = 50; * let y = 50; * * // Declare the setup() method. * p.setup = function () { * p.createCanvas(100, 100); * * p.describe('A white circle moves randomly on a gray background.'); * }; * * // Declare the draw() method. * p.draw = function () { * p.background(200); * * // Update x and y. * x += p.random(-1, 1); * y += p.random(-1, 1); * * // Draw the circle. * p.circle(x, y, 20); * }; * } * * // Initialize the sketch. * new p5(sketch); * * @example * // META:norender * // Declare the function containing the sketch. * function sketch(p) { * * // Declare the setup() method. * p.setup = function () { * p.createCanvas(100, 100); * * p.describe('A white circle drawn on a gray background.'); * }; * * // Declare the draw() method. * p.draw = function () { * p.background(200); * * // Draw the circle. * p.circle(50, 50, 20); * }; * } * * // Select the web page's body element. * let body = document.querySelector('body'); * * // Initialize the sketch and attach it to the web page's body. * new p5(sketch, body); * * @example * // META:norender * // Declare the function containing the sketch. * function sketch(p) { * * // Declare the setup() method. * p.setup = function () { * p.createCanvas(100, 100); * * p.describe( * 'A white circle drawn on a gray background. The circle follows the mouse as the user moves.' * ); * }; * * // Declare the draw() method. * p.draw = function () { * p.background(200); * * // Draw the circle. * p.circle(p.mouseX, p.mouseY, 20); * }; * } * * // Initialize the sketch. * new p5(sketch); * * @example * // META:norender * // Declare the function containing the sketch. * function sketch(p) { * * // Declare the setup() method. * p.setup = function () { * p.createCanvas(100, 100); * * p.describe( * 'A white circle drawn on a gray background. The circle follows the mouse as the user moves. The circle becomes black when the user double-clicks.' * ); * }; * * // Declare the draw() method. * p.draw = function () { * p.background(200); * * // Draw the circle. * p.circle(p.mouseX, p.mouseY, 20); * }; * * // Declare the doubleClicked() method. * p.doubleClicked = function () { * // Change the fill color when the user double-clicks. * p.fill(0); * }; * } * * // Initialize the sketch. * new p5(sketch); */ } if(typeof p5 !== 'undefined'){ structure(p5, p5.prototype); } /** * @module Environment * @submodule Environment * @for p5 * @requires core * @requires constants */ // import { Vector } from '../math/p5.Vector'; function environment$1(p5, fn, lifecycles){ const standardCursors = [ARROW, CROSS, HAND, MOVE, TEXT, WAIT]; fn._frameRate = 0; fn._lastFrameTime = window.performance.now(); fn._targetFrameRate = 60; const _windowPrint = window.print; let windowPrintDisabled = false; lifecycles.presetup = function(){ const events = [ 'resize' ]; for(const event of events){ window.addEventListener(event, this[`_on${event}`].bind(this), { passive: false, signal: this._removeSignal }); } }; /** * Displays text in the web browser's console. * * `print()` is helpful for printing values while debugging. Each call to * `print()` creates a new line of text. * * Note: Call `print('\n')` to print a blank line. Calling `print()` without * an argument opens the browser's dialog for printing documents. * * @method print * @param {Any} contents content to print to the console. * @example * // META:norender * function setup() { * // Prints "hello, world" to the console. * print('hello, world'); * } * * @example * // META:norender * function setup() { * let name = 'ada'; * // Prints "hello, ada" to the console. * print(`hello, ${name}`); * } */ fn.print = function(...args) { if (!args.length) { if (!windowPrintDisabled) { _windowPrint(); if ( window.confirm( 'You just tried to print the webpage. Do you want to prevent this from running again?' ) ) { windowPrintDisabled = true; } } } else { console.log(...args); } }; /** * A `Number` variable that tracks the number of frames drawn since the sketch * started. * * `frameCount`'s value is 0 inside setup(). It * increments by 1 each time the code in draw() * finishes executing. * * @property {Integer} frameCount * @readOnly * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Display the value of * // frameCount. * textSize(30); * textAlign(CENTER, CENTER); * text(frameCount, 50, 50); * * describe('The number 0 written in black in the middle of a gray square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Set the frameRate to 30. * frameRate(30); * * textSize(30); * textAlign(CENTER, CENTER); * * describe('A number written in black in the middle of a gray square. Its value increases rapidly.'); * } * * function draw() { * background(200); * * // Display the value of * // frameCount. * text(frameCount, 50, 50); * } */ fn.frameCount = 0; /** * A `Number` variable that tracks the number of milliseconds it took to draw * the last frame. * * `deltaTime` contains the amount of time it took * draw() to execute during the previous frame. It's * useful for simulating physics. * * @property {Integer} deltaTime * @readOnly * @example * let x = 0; * let speed = 0.05; * * function setup() { * createCanvas(100, 100); * * // Set the frameRate to 30. * frameRate(30); * * describe('A white circle moves from left to right on a gray background. It reappears on the left side when it reaches the right side.'); * } * * function draw() { * background(200); * * // Use deltaTime to calculate * // a change in position. * let deltaX = speed * deltaTime; * * // Update the x variable. * x += deltaX; * * // Reset x to 0 if it's * // greater than 100. * if (x > 100) { * x = 0; * } * * // Use x to set the circle's * // position. * circle(x, 50, 20); * } */ fn.deltaTime = 0; /** * A `Boolean` variable that's `true` if the browser is focused and `false` if * not. * * Note: The browser window can only receive input if it's focused. * * @property {Boolean} focused * @readOnly * @example * // Open this example in two separate browser * // windows placed side-by-side to demonstrate. * * function setup() { * createCanvas(100, 100); * * describe('A square changes color from green to red when the browser window is out of focus.'); * } * * function draw() { * // Change the background color * // when the browser window * // goes in/out of focus. * if (focused === true) { * background(0, 255, 0); * } else { * background(255, 0, 0); * } * } */ fn.focused = document.hasFocus(); /** * Changes the cursor's appearance. * * The first parameter, `type`, sets the type of cursor to display. The * built-in options are `ARROW`, `CROSS`, `HAND`, `MOVE`, `TEXT`, and `WAIT`. * `cursor()` also recognizes standard CSS cursor properties passed as * strings: `'help'`, `'wait'`, `'crosshair'`, `'not-allowed'`, `'zoom-in'`, * and `'grab'`. If the path to an image is passed, as in * `cursor('assets/target.png')`, then the image will be used as the cursor. * Images must be in .cur, .gif, .jpg, .jpeg, or .png format and should be at most 32 by 32 pixels large. * * The parameters `x` and `y` are optional. If an image is used for the * cursor, `x` and `y` set the location pointed to within the image. They are * both 0 by default, so the cursor points to the image's top-left corner. `x` * and `y` must be less than the image's width and height, respectively. * * @method cursor * @param {(ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String)} type Built-in: either ARROW, CROSS, HAND, MOVE, TEXT, or WAIT. * Native CSS properties: 'grab', 'progress', and so on. * Path to cursor image. * @param {Number} [x] horizontal active spot of the cursor. * @param {Number} [y] vertical active spot of the cursor. * @example * function setup() { * createCanvas(100, 100); * * describe('A gray square. The cursor appears as crosshairs.'); * } * * function draw() { * background(200); * * // Set the cursor to crosshairs: + * cursor(CROSS); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A gray square divided into quadrants. The cursor image changes when the mouse moves to each quadrant.'); * } * * function draw() { * background(200); * * // Divide the canvas into quadrants. * line(50, 0, 50, 100); * line(0, 50, 100, 50); * * // Change cursor based on mouse position. * if (mouseX < 50 && mouseY < 50) { * cursor(CROSS); * } else if (mouseX > 50 && mouseY < 50) { * cursor('progress'); * } else if (mouseX > 50 && mouseY > 50) { * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16'); * } else { * cursor('grab'); * } * } * * @example * function setup() { * createCanvas(100, 100); * * describe('An image of three purple curves follows the mouse. The image shifts when the mouse is pressed.'); * } * * function draw() { * background(200); * * // Change the cursor's active spot * // when the mouse is pressed. * if (mouseIsPressed === true) { * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16', 8, 8); * } else { * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16'); * } * } */ fn.cursor = function(type, x, y) { let cursor = 'auto'; const canvas = this._curElement.elt; if (standardCursors.includes(type)) { // Standard css cursor cursor = type; } else if (typeof type === 'string') { let coords = ''; if (typeof x === 'number') { // fix for #8323 y = typeof y === 'number' ? y : 0; // Note that x and y values must be unit-less positive integers < 32 // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor coords = `${Math.max(x, 0)} ${Math.max(y, 0)}`; } if ( type.substring(0, 7) === 'http://' || type.substring(0, 8) === 'https://' ) { // Image (absolute url) cursor = `url(${type}) ${coords}, auto`; } else if (/\.(cur|jpg|jpeg|gif|png|CUR|JPG|JPEG|GIF|PNG)$/.test(type)) { // Image file (relative path) - Separated for performance reasons cursor = `url(${type}) ${coords}, auto`; } else { // Any valid string for the css cursor property cursor = type; } } canvas.style.cursor = cursor; }; /** * Sets the number of frames to draw per second. * * Calling `frameRate()` with one numeric argument, as in `frameRate(30)`, * attempts to draw 30 frames per second (FPS). The target frame rate may not * be achieved depending on the sketch's processing needs. Most computers * default to a frame rate of 60 FPS. Frame rates of 24 FPS and above are * fast enough for smooth animations. * * Calling `frameRate()` without an argument returns the current frame rate. * The value returned is an approximation. * * @method frameRate * @param {Number} fps number of frames to draw per second. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * describe('A white circle on a gray background. The circle moves from left to right in a loop. It slows down when the mouse is pressed.'); * } * * function draw() { * background(200); * * // Set the x variable based * // on the current frameCount. * let x = frameCount % 100; * * // If the mouse is pressed, * // decrease the frame rate. * if (mouseIsPressed === true) { * frameRate(10); * } else { * frameRate(60); * } * * // Use x to set the circle's * // position. * circle(x, 50, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A number written in black on a gray background. The number decreases when the mouse is pressed.'); * } * * function draw() { * background(200); * * // If the mouse is pressed, do lots * // of math to slow down drawing. * if (mouseIsPressed === true) { * for (let i = 0; i < 1000000; i += 1) { * random(); * } * } * * // Get the current frame rate * // and display it. * let fps = frameRate(); * text(fps, 50, 50); * } */ /** * @method frameRate * @return {Number} current frame rate. */ fn.frameRate = function(fps) { // p5._validateParameters('frameRate', arguments); if (typeof fps !== 'number' || fps < 0) { return this._frameRate; } else { this._targetFrameRate = fps; if (fps === 0) { this._frameRate = fps; } return this; } }; /** * Returns the current framerate. * * @private * @return {Number} current frameRate */ fn.getFrameRate = function() { return this.frameRate(); }; /** * Specifies the number of frames to be displayed every second. For example, * the function call frameRate(30) will attempt to refresh 30 times a second. * If the processor is not fast enough to maintain the specified rate, the * frame rate will not be achieved. Setting the frame rate within setup() is * recommended. The default rate is 60 frames per second. * * Calling `frameRate()` with no arguments returns the current frame rate. * * @private * @param {Number} [fps] number of frames to be displayed every second */ fn.setFrameRate = function(fps) { return this.frameRate(fps); }; /** * Returns the target frame rate. * * The value is either the system frame rate or the last value passed to * frameRate(). * * @method getTargetFrameRate * @return {Number} _targetFrameRate * @example * function setup() { * createCanvas(100, 100); * * describe('The number 20 written in black on a gray background.'); * } * * function draw() { * background(200); * * // Set the frame rate to 20. * frameRate(20); * * // Get the target frame rate and * // display it. * let fps = getTargetFrameRate(); * text(fps, 43, 54); * } */ fn.getTargetFrameRate = function() { return this._targetFrameRate; }; /** * Hides the cursor from view. * * @method noCursor * @example * function setup() { * // Hide the cursor. * noCursor(); * } * * function draw() { * background(200); * * circle(mouseX, mouseY, 10); * * describe('A white circle on a gray background. The circle follows the mouse as it moves. The cursor is hidden.'); * } */ fn.noCursor = function() { this._curElement.elt.style.cursor = 'none'; }; /** * A `String` variable with the WebGL version in use. * * `webglVersion`'s value equals one of the following string constants: * * - `WEBGL2` whose value is `'webgl2'`, * - `WEBGL` whose value is `'webgl'`, or * - `P2D` whose value is `'p2d'`. This is the default for 2D sketches. * - `P2DHDR` whose value is `'p2d-hdr'` (used for HDR 2D sketches, if available). * * See setAttributes() for ways to set the * WebGL version. * * @property {(WEBGL|WEBGL2)} webglVersion * @readOnly * @example * function setup() { * background(200); * * // Display the current WebGL version. * text(webglVersion, 42, 54); * * describe('The text "p2d" written in black on a gray background.'); * } * * @example * let font; * * async function setup() { * // Load a font to use. * font = await loadFont('assets/inconsolata.otf'); * * // Create a canvas using WEBGL mode. * createCanvas(100, 50, WEBGL); * background(200); * * // Display the current WebGL version. * fill(0); * textFont(font); * text(webglVersion, -15, 5); * * describe('The text "webgl2" written in black on a gray background.'); * } * * @example * let font; * * async function setup() { * // Load a font to use. * font = await loadFont('assets/inconsolata.otf'); * * // Create a canvas using WEBGL mode. * createCanvas(100, 50, WEBGL); * * // Set WebGL to version 1. * setAttributes({ version: 1 }); * * background(200); * * // Display the current WebGL version. * fill(0); * textFont(font); * text(webglVersion, -14, 5); * * describe('The text "webgl" written in black on a gray background.'); * } */ fn.webglVersion = P2D; /** * A `Number` variable that stores the width of the screen display. * * `displayWidth` is useful for running full-screen programs. Its value * depends on the current pixelDensity(). * * Note: The actual screen width can be computed as * `displayWidth * pixelDensity()`. * * @property {Number} displayWidth * @readOnly * @example * // META:norender * function setup() { * // Set the canvas' width and height * // using the display's dimensions. * createCanvas(displayWidth, displayHeight); * * background(200); * * describe('A gray canvas that is the same size as the display.'); * } * * @alt * This example does not render anything. */ fn.displayWidth = screen.width; /** * A `Number` variable that stores the height of the screen display. * * `displayHeight` is useful for running full-screen programs. Its value * depends on the current pixelDensity(). * * Note: The actual screen height can be computed as * `displayHeight * pixelDensity()`. * * @property {Number} displayHeight * @readOnly * @example * // META:norender * function setup() { * // Set the canvas' width and height * // using the display's dimensions. * createCanvas(displayWidth, displayHeight); * * background(200); * * describe('A gray canvas that is the same size as the display.'); * } * * @alt * This example does not render anything. */ fn.displayHeight = screen.height; /** * A `Number` variable that stores the width of the browser's viewport. * * The layout viewport * is the area within the browser that's available for drawing. * * @property {Number} windowWidth * @readOnly * @example * // META:norender * function setup() { * // Set the canvas' width and height * // using the browser's dimensions. * createCanvas(windowWidth, windowHeight); * * background(200); * * describe('A gray canvas that takes up the entire browser window.'); * } * * @alt * This example does not render anything. */ fn.windowWidth = 0; /** * A `Number` variable that stores the height of the browser's viewport. * * The layout viewport * is the area within the browser that's available for drawing. * * @property {Number} windowHeight * @readOnly * @example * // META:norender * function setup() { * // Set the canvas' width and height * // using the browser's dimensions. * createCanvas(windowWidth, windowHeight); * * background(200); * * describe('A gray canvas that takes up the entire browser window.'); * } * * @alt * This example does not render anything. */ fn.windowHeight = 0; /** * A function that's called when the browser window is resized. * * Code placed in the body of `windowResized()` will run when the * browser window's size changes. It's a good place to call * resizeCanvas() or make other * adjustments to accommodate the new window size. * * The `event` parameter is optional. If added to the function declaration, it * can be used for debugging or other purposes. * * @method windowResized * @param {Event} [event] optional resize Event. * @example * // META:norender * function setup() { * createCanvas(windowWidth, windowHeight); * * describe('A gray canvas with a white circle at its center. The canvas takes up the entire browser window. It changes size to match the browser window.'); * } * * function draw() { * background(200); * * // Draw a circle at the center. * circle(width / 2, height / 2, 50); * } * * // Resize the canvas when the * // browser's size changes. * function windowResized() { * resizeCanvas(windowWidth, windowHeight); * } * * @alt * This example does not render anything. * * @example * // META:norender * function setup() { * createCanvas(windowWidth, windowHeight); * } * * function draw() { * background(200); * * describe('A gray canvas that takes up the entire browser window. It changes size to match the browser window.'); * } * * function windowResized(event) { * // Resize the canvas when the * // browser's size changes. * resizeCanvas(windowWidth, windowHeight); * * // Print the resize event to the console for debugging. * print(event); * } * * @alt * This example does not render anything. */ fn._onresize = function(e) { this.windowWidth = getWindowWidth(); this.windowHeight = getWindowHeight(); let executeDefault; if (typeof this._customActions.windowResized === 'function') { executeDefault = this._customActions.windowResized(e); if (executeDefault !== undefined && !executeDefault) { e.preventDefault(); } } }; function getWindowWidth() { return ( window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || (document.body && document.body.clientWidth) || 0 ); } function getWindowHeight() { return ( window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || (document.body && document.body.clientHeight) || 0 ); } /** * Called upon each p5 instantiation instead of module import due to the * possibility of the window being resized when no sketch is active. */ fn._updateWindowSize = function() { this.windowWidth = getWindowWidth(); this.windowHeight = getWindowHeight(); }; Object.defineProperty(fn, 'width', { get(){ return this._renderer.width; } }); Object.defineProperty(fn, 'height', { get(){ return this._renderer.height; } }); /** * Toggles full-screen mode or returns the current mode. * * Calling `fullscreen(true)` makes the sketch full-screen. Calling * `fullscreen(false)` makes the sketch its original size. * * Calling `fullscreen()` without an argument returns `true` if the sketch * is in full-screen mode and `false` if not. * * Note: Due to browser restrictions, `fullscreen()` can only be called with * user input such as a mouse press. * * @method fullscreen * @param {Boolean} [val] whether the sketch should be in fullscreen mode. * @return {Boolean} current fullscreen state. * @example * function setup() { * background(200); * * describe('A gray canvas that switches between default and full-screen display when clicked.'); * } * * // If the mouse is pressed, * // toggle full-screen mode. * function mousePressed() { * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { * let fs = fullscreen(); * fullscreen(!fs); * } * } */ fn.fullscreen = function(val) { // p5._validateParameters('fullscreen', arguments); // no arguments, return fullscreen or not if (typeof val === 'undefined') { return ( document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement ); } else { // otherwise set to fullscreen or not if (val) { launchFullscreen(document.documentElement); } else { exitFullscreen(); } } }; /** * Sets the pixel density or returns the current density. * * Computer displays are grids of little lights called pixels. A * display's pixel density describes how many pixels it packs into an * area. Displays with smaller pixels have a higher pixel density and create * sharper images. * * `pixelDensity()` sets the pixel scaling for high pixel density displays. * By default, the pixel density is set to match the display's density. * Calling `pixelDensity(1)` turn this off. * * Calling `pixelDensity()` without an argument returns the current pixel * density. * * @method pixelDensity * @param {Number} [val] desired pixel density. * @chainable * @example * function setup() { * // Set the pixel density to 1. * pixelDensity(1); * * // Create a canvas and draw * // a circle. * createCanvas(100, 100); * background(200); * circle(50, 50, 70); * * describe('A fuzzy white circle on a gray canvas.'); * } * * @example * function setup() { * // Set the pixel density to 3. * pixelDensity(3); * * // Create a canvas, paint the * // background, and draw a * // circle. * createCanvas(100, 100); * background(200); * circle(50, 50, 70); * * describe('A sharp white circle on a gray canvas.'); * } */ /** * @method pixelDensity * @returns {Number} current pixel density of the sketch. */ fn.pixelDensity = function(val) { // p5._validateParameters('pixelDensity', arguments); let returnValue; if (typeof val === 'number') { if (val !== this._renderer._pixelDensity) { this._renderer._pixelDensity = val; } returnValue = this; this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas } else { returnValue = this._renderer._pixelDensity; } return returnValue; }; /** * Returns the display's current pixel density. * * @method displayDensity * @returns {Number} current pixel density of the display. * @example * function setup() { * // Set the pixel density to 1. * pixelDensity(1); * * // Create a canvas and draw * // a circle. * createCanvas(100, 100); * background(200); * circle(50, 50, 70); * * describe('A fuzzy white circle drawn on a gray background. The circle becomes sharper when the mouse is pressed.'); * } * * function mousePressed() { * // Get the current display density. * let d = displayDensity(); * * // Use the display density to set * // the sketch's pixel density. * pixelDensity(d); * * // Paint the background and * // draw a circle. * background(200); * circle(50, 50, 70); * } */ fn.displayDensity = () => window.devicePixelRatio; function launchFullscreen(element) { const enabled = document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled; if (!enabled) { throw new Error('Fullscreen not enabled in this browser.'); } if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } } function exitFullscreen() { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } /** * Returns the sketch's current * URL * as a `String`. * * @method getURL * @return {String} url * @example * function setup() { * background(200); * * // Get the sketch's URL * // and display it. * let url = getURL(); * textWrap(CHAR); * text(url, 0, 40, 100); * * describe('The URL "https://p5js.org/reference/p5/getURL" written in black on a gray background.'); * } */ fn.getURL = () => location.href; /** * Returns the current * URL * path as an `Array` of `String`s. * * For example, consider a sketch hosted at the URL * `https://example.com/sketchbook`. Calling `getURLPath()` returns * `['sketchbook']`. For a sketch hosted at the URL * `https://example.com/sketchbook/monday`, `getURLPath()` returns * `['sketchbook', 'monday']`. * * @method getURLPath * @return {String[]} path components. * @example * function setup() { * background(200); * * // Get the sketch's URL path * // and display the first * // part. * let path = getURLPath(); * text(path[0], 25, 54); * * describe('The word "reference" written in black on a gray background.'); * } */ fn.getURLPath = () => location.pathname.split('/').filter(v => v !== ''); /** * Returns the current * URL parameters * in an `Object`. * * For example, calling `getURLParams()` in a sketch hosted at the URL * `https://p5js.org?year=2014&month=May&day=15` returns * `{ year: 2014, month: 'May', day: 15 }`. * * @method getURLParams * @return {Object} URL params * @example * // META:norender * // Imagine this sketch is hosted at the following URL: * // https://p5js.org?year=2014&month=May&day=15 * * function setup() { * background(200); * * // Get the sketch's URL * // parameters and display * // them. * let params = getURLParams(); * text(params.day, 10, 20); * text(params.month, 10, 40); * text(params.year, 10, 60); * * describe('The text "15", "May", and "2014" written in black on separate lines.'); * } * * @alt * This example does not render anything. */ fn.getURLParams = function() { const re = /[?&]([^&=]+)(?:[&=])([^&=]+)/gim; let m; const v = {}; while ((m = re.exec(location.search)) != null) { if (m.index === re.lastIndex) { re.lastIndex++; } v[m[1]] = m[2]; } return v; }; /** * Converts 3D world coordinates to 2D screen coordinates. * * This function takes a 3D vector and converts its coordinates * from the world space to screen space. This can be useful for placing * 2D elements in a 3D scene or for determining the screen position * of 3D objects. * * @method worldToScreen * @param {Number|p5.Vector} x The x coordinate in world space. (Or a vector for all three coordinates.) * @param {Number} y The y coordinate in world space. * @param {Number} [z] The z coordinate in world space. * @return {p5.Vector} A vector containing the 2D screen coordinates. * @example * function setup() { * createCanvas(150, 150); * let vertices = [ * createVector(-20, -20), * createVector(20, -20), * createVector(20, 20), * createVector(-20, 20) * ]; * * push(); * translate(75, 55); * rotate(PI / 4); * * // Convert world coordinates to screen coordinates * let screenPos = vertices.map(v => worldToScreen(v)); * pop(); * * background(200); * * stroke(0); * fill(100, 150, 255, 100); * beginShape(); * screenPos.forEach(pos => vertex(pos.x, pos.y)); * endShape(CLOSE); * * screenPos.forEach((pos, i) => { * fill(0); * textSize(10); * if (i === 0) { * text(i + 1, pos.x + 3, pos.y - 7); * } else if (i === 1) { * text(i + 1, pos.x + 7, pos.y + 2); * } else if (i === 2) { * text(i + 1, pos.x - 2, pos.y + 12); * } else if (i === 3) { * text(i + 1, pos.x - 12, pos.y - 2); * } * }); * * fill(0); * noStroke(); * textSize(10); * let legendY = height - 35; * screenPos.forEach((pos, i) => { * text(`Vertex ${i + 1}: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)})`, 5, legendY + i * 10); * }); * * describe('A rotating square is transformed and drawn using screen coordinates.'); * * } * * @example * let vertices; * * function setup() { * createCanvas(100, 100, WEBGL); * vertices = [ * createVector(-25, -25, -25), * createVector(25, -25, -25), * createVector(25, 25, -25), * createVector(-25, 25, -25), * createVector(-25, -25, 25), * createVector(25, -25, 25), * createVector(25, 25, 25), * createVector(-25, 25, 25) * ]; * * describe('A rotating cube with points mapped to 2D screen space and displayed as ellipses.'); * * } * * function draw() { * background(200); * * // Animate rotation * let rotationX = millis() / 1000; * let rotationY = millis() / 1200; * * push(); * * rotateX(rotationX); * rotateY(rotationY); * * // Convert world coordinates to screen coordinates * let screenPos = vertices.map(v => worldToScreen(v)); * * pop(); * * screenPos.forEach((pos, i) => { * * let screenX = pos.x - width / 2; * let screenY = pos.y - height / 2; * fill(0); * noStroke(); * ellipse(screenX, screenY, 3, 3); * }); * } */ fn.worldToScreen = function(worldPosition) { if (typeof worldPosition === 'number') { // We got passed numbers, convert to vector worldPosition = this.createVector(...arguments); } const matrix = this._renderer.getWorldToScreenMatrix(); const screenPosition = matrix.multiplyAndNormalizePoint(worldPosition); return screenPosition; }; /** * Converts 2D screen coordinates to 3D world coordinates. * * This function takes a vector and converts its coordinates from coordinates * on the screen to coordinates in the currently drawn object. This can be * useful for determining the mouse position relative to a 2D or 3D object. * * If given, the Z component of the input coordinates is treated as "depth", * or distance from the camera. * * @method screenToWorld * @param {Number|p5.Vector} x The x coordinate in screen space. (Or a vector for all three coordinates.) * @param {Number} y The y coordinate in screen space. * @param {Number} [z] The z coordinate in screen space. * @return {p5.Vector} A vector containing the 3D world space coordinates. * @example * function setup() { * createCanvas(100, 100); * describe('A rotating square with a line passing through the mouse drawn across it.'); * } * * function draw() { * background(220); * * // Move to center and rotate * translate(width/2, height/2); * rotate(millis() / 1000); * rect(-30, -30, 60); * * // Compute the location of the mouse in the coordinates of the square * let localMouse = screenToWorld(createVector(mouseX, mouseY)); * * // Draw a line parallel to the local Y axis, passing through the mouse * line(localMouse.x, -30, localMouse.x, 30); * } */ fn.screenToWorld = function(screenPosition) { if (typeof screenPosition === 'number') { // We got passed numbers, convert to vector screenPosition = this.createVector(...arguments); } const matrix = this._renderer.getWorldToScreenMatrix(); if (screenPosition.dimensions === 2) { // Calculate a sensible Z value for the current camera projection that // will result in 0 once converted to world coordinates let z = matrix.mat4[14] / matrix.mat4[15]; screenPosition = this.createVector(screenPosition.x, screenPosition.y, z); } const matrixInverse = matrix.invert(matrix); const worldPosition = matrixInverse .multiplyAndNormalizePoint(screenPosition); return worldPosition; }; /** * A `Number` variable that stores the width of the canvas in pixels. * * `width`'s default value is 100. Calling * createCanvas() or * resizeCanvas() changes the value of * `width`. Calling noCanvas() sets its value to * 0. * * @example * function setup() { * background(200); * * // Display the canvas' width. * text(width, 42, 54); * * describe('The number 100 written in black on a gray square.'); * } * * @example * function setup() { * createCanvas(50, 100); * * background(200); * * // Display the canvas' width. * text(width, 21, 54); * * describe('The number 50 written in black on a gray rectangle.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Display the canvas' width. * text(width, 42, 54); * * describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.'); * } * * // If the mouse is pressed, reisze * // the canvas and display its new * // width. * function mousePressed() { * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { * resizeCanvas(50, 100); * background(200); * text(width, 21, 54); * } * } * * @property {Number} width * @readOnly */ /** * A `Number` variable that stores the height of the canvas in pixels. * * `height`'s default value is 100. Calling * createCanvas() or * resizeCanvas() changes the value of * `height`. Calling noCanvas() sets its value to * 0. * * @example * function setup() { * background(200); * * // Display the canvas' height. * text(height, 42, 54); * * describe('The number 100 written in black on a gray square.'); * } * * @example * function setup() { * createCanvas(100, 50); * * background(200); * * // Display the canvas' height. * text(height, 42, 27); * * describe('The number 50 written in black on a gray rectangle.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Display the canvas' height. * text(height, 42, 54); * * describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.'); * } * * // If the mouse is pressed, reisze * // the canvas and display its new * // height. * function mousePressed() { * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { * resizeCanvas(100, 50); * background(200); * text(height, 42, 27); * } * } * * @property {Number} height * @readOnly */ } if(typeof p5 !== 'undefined'){ environment$1(p5, p5.prototype); } /** @import { Matrix3x3, Vector3 } from "./types.js" */ // dot3 and transform functions adapted from https://github.com/texel-org/color/blob/9793c7d4d02b51f068e0f3fd37131129a4270396/src/core.js // // The MIT License (MIT) // Copyright (c) 2024 Matt DesLauriers // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE // OR OTHER DEALINGS IN THE SOFTWARE. /** * Returns the dot product of two vectors each with a length of 3. * * @param {Vector3} a * @param {Vector3} b * @returns {number} */ function dot3 (a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } /** * Transforms a vector of length 3 by a 3x3 matrix. Specify the same input and output * vector to transform in place. * * @param {Vector3} input * @param {Matrix3x3} matrix * @param {Vector3} [out] * @returns {Vector3} */ function multiply_v3_m3x3 (input, matrix, out = [0, 0, 0]) { const x = dot3(input, matrix[0]); const y = dot3(input, matrix[1]); const z = dot3(input, matrix[2]); out[0] = x; out[1] = y; out[2] = z; return out; } /** * Various utility functions */ /** * Check if a value is a string (including a String object) * @param {any} str - Value to check * @returns {str is string} */ function isString (str) { return type$1(str) === "string"; } /** * Determine the internal JavaScript [[Class]] of an object. * @param {any} o - Value to check * @returns {string} */ function type$1 (o) { let str = Object.prototype.toString.call(o); return (str.match(/^\[object\s+(.*?)\]$/)[1] || "").toLowerCase(); } /** * @param {number} n * @param {{ precision?: number | undefined, unit?: string | undefined }} options * @returns {string} */ function serializeNumber (n, { precision = 16, unit }) { if (isNone(n)) { return "none"; } n = +toPrecision(n, precision); return n + (unit ?? ""); } /** * Check if a value corresponds to a none argument * @param {any} n - Value to check * @returns {n is null} */ function isNone (n) { return n === null; } /** * Round a number to a certain number of significant digits * @param {number} n - The number to round * @param {number} precision - Number of significant digits */ function toPrecision (n, precision) { if (n === 0) { return 0; } let integer = ~~n; let digits = 0; if (integer && precision) { digits = ~~Math.log10(Math.abs(integer)) + 1; } const multiplier = 10.0 ** (precision - digits); return Math.floor(n * multiplier + 0.5) / multiplier; } /** * @param {number} start * @param {number} end * @param {number} p */ function interpolate (start, end, p) { if (isNaN(start)) { return end; } if (isNaN(end)) { return start; } return start + (end - start) * p; } /** * @param {number} start * @param {number} end * @param {number} value */ function interpolateInv (start, end, value) { return (value - start) / (end - start); } /** * @param {[number, number]} from * @param {[number, number]} to * @param {number} value */ function mapRange (from, to, value) { if ( !from || !to || from === to || (from[0] === to[0] && from[1] === to[1]) || isNaN(value) || value === null ) { // Ranges missing or the same return value; } return interpolate(to[0], to[1], interpolateInv(from[0], from[1], value)); } /** * Clamp value between the minimum and maximum * @param {number} min minimum value to return * @param {number} val the value to return if it is between min and max * @param {number} max maximum value to return */ function clamp (min, val, max) { return Math.max(Math.min(max, val), min); } /** * Copy sign of one value to another. * @param {number} to - Number to copy sign to * @param {number} from - Number to copy sign from */ function copySign (to, from) { return Math.sign(to) === Math.sign(from) ? to : -to; } /** * Perform pow on a signed number and copy sign to result * @param {number} base The base number * @param {number} exp The exponent */ function spow (base, exp) { return copySign(Math.abs(base) ** exp, base); } /** * Perform a divide, but return zero if the denominator is zero * @param {number} n The numerator * @param {number} d The denominator */ function zdiv (n, d) { return d === 0 ? 0 : n / d; } /** * Perform a bisect on a sorted list and locate the insertion point for * a value in arr to maintain sorted order. * @param {number[]} arr - array of sorted numbers * @param {number} value - value to find insertion point for * @param {number} lo - used to specify a the low end of a subset of the list * @param {number} hi - used to specify a the high end of a subset of the list */ function bisectLeft (arr, value, lo = 0, hi = arr.length) { while (lo < hi) { const mid = (lo + hi) >> 1; if (arr[mid] < value) { lo = mid + 1; } else { hi = mid; } } return lo; } /** * Determines whether an argument is an instance of a constructor, including subclasses. * This is done by first just checking `instanceof`, * and then comparing the string names of the constructors if that fails. * @param {any} arg * @param {C} constructor * @template {new (...args: any) => any} C * @returns {arg is InstanceType} */ function isInstance (arg, constructor) { if (arg instanceof constructor) { return true; } const targetName = constructor.name; while (arg) { const proto = Object.getPrototypeOf(arg); const constructorName = proto?.constructor?.name; if (constructorName === targetName) { return true; } if (!constructorName || constructorName === "Object") { return false; } arg = proto; } return false; } class Type { // Class properties - declared here so that type inference works type; coordMeta; coordRange; /** @type {[number, number]} */ range; /** * @param {any} type * @param {import("./types.js").CoordMeta} coordMeta */ constructor (type, coordMeta) { if (typeof type === "object") { this.coordMeta = type; } if (coordMeta) { this.coordMeta = coordMeta; this.coordRange = coordMeta.range ?? coordMeta.refRange; } if (typeof type === "string") { let params = type .trim() .match(/^(?<[a-z]+>)(\[(?-?[.\d]+),\s*(?-?[.\d]+)\])?$/); if (!params) { throw new TypeError(`Cannot parse ${type} as a type definition.`); } this.type = params.groups.type; let { min, max } = params.groups; if (min || max) { this.range = [+min, +max]; } } } /** @returns {[number, number]} */ get computedRange () { if (this.range) { return this.range; } if (this.type === "") { return this.percentageRange(); } else if (this.type === "") { return [0, 360]; } return null; } get unit () { if (this.type === "") { return "%"; } else if (this.type === "") { return "deg"; } return ""; } /** * Map a number to the internal representation * @param {number} number */ resolve (number) { if (this.type === "") { return number; } let fromRange = this.computedRange; let toRange = this.coordRange; if (this.type === "") { toRange ??= this.percentageRange(); } return mapRange(fromRange, toRange, number); } /** * Serialize a number from the internal representation to a string * @param {number} number * @param {number} [precision] */ serialize (number, precision) { let toRange = this.type === "" ? this.percentageRange(100) : this.computedRange; let unit = this.unit; number = mapRange(this.coordRange, toRange, number); return serializeNumber(number, { unit, precision }); } toString () { let ret = this.type; if (this.range) { let [min = "", max = ""] = this.range; ret += `[${min},${max}]`; } return ret; } /** * Returns a percentage range for values of this type * @param {number} scale * @returns {[number, number]} */ percentageRange (scale = 1) { let range; if ( (this.coordMeta && this.coordMeta.range) || (this.coordRange && this.coordRange[0] >= 0) ) { range = [0, 1]; } else { range = [-1, 1]; } return [range[0] * scale, range[1] * scale]; } static get (type, coordMeta) { if (isInstance(type, this)) { return type; } return new this(type, coordMeta); } } /** @import { ColorSpace, Coords } from "./types.js" */ // Type re-exports /** @typedef {import("./types.js").Format} FormatInterface */ /** * @internal * Used to index {@link FormatInterface Format} objects and store an instance. * Not meant for external use */ const instance = Symbol("instance"); /** * Remove the first element of an array type * @template {any[]} T * @typedef {T extends [any, ...infer R] ? R : T[number][]} RemoveFirstElement */ /** * @class Format * @implements {Omit} * Class to hold a color serialization format */ class Format { // Class properties - declared here so that type inference works type; name; spaceCoords; /** @type {Type[][]} */ coords; /** @type {string | undefined} */ id; /** @type {boolean | undefined} */ alpha; /** * @param {FormatInterface} format * @param {ColorSpace} space */ constructor (format, space = format.space) { format[instance] = this; this.type = "function"; this.name = "color"; Object.assign(this, format); this.space = space; if (this.type === "custom") { // Nothing else to do here return; } this.spaceCoords = Object.values(space.coords); if (!this.coords) { // @ts-expect-error Strings are converted to the correct type later this.coords = this.spaceCoords.map(coordMeta => { let ret = ["", ""]; if (coordMeta.type === "angle") { ret.push(""); } return ret; }); } this.coords = this.coords.map( /** @param {string | string[] | Type[]} types */ (types, i) => { let coordMeta = this.spaceCoords[i]; if (typeof types === "string") { types = types.trim().split(/\s*\|\s*/); } return types.map(type => Type.get(type, coordMeta)); }, ); } /** * @param {Coords} coords * @param {number} precision * @param {Type[]} types */ serializeCoords (coords, precision, types) { types = coords.map((_, i) => Type.get(types?.[i] ?? this.coords[i][0], this.spaceCoords[i])); return coords.map((c, i) => types[i].serialize(c, precision)); } /** * Validates the coordinates of a color against a format's coord grammar and * maps the coordinates to the range or refRange of the coordinates. * @param {Coords} coords * @param {[string, string, string]} types */ coerceCoords (coords, types) { return Object.entries(this.space.coords).map(([id, coordMeta], i) => { let arg = coords[i]; if (isNone(arg) || isNaN(arg)) { // Nothing to do here return arg; } // Find grammar alternative that matches the provided type // Non-strict equals is intentional because we are comparing w/ string objects let providedType = types[i]; let type = this.coords[i].find(c => c.type == providedType); // Check that each coord conforms to its grammar if (!type) { // Type does not exist in the grammar, throw let coordName = coordMeta.name || id; throw new TypeError( `${providedType ?? /** @type {any} */ (arg)?.raw ?? arg} not allowed for ${coordName} in ${this.name}()`, ); } arg = type.resolve(arg); if (type.range) { // Adjust type to include range types[i] = type.toString(); } return arg; }); } /** * @returns {boolean | Required["serialize"]} */ canSerialize () { return this.type === "function" || /** @type {any} */ (this).serialize; } /** * @param {string} str * @returns {(import("./types.js").ColorConstructor) | undefined | null} */ parse (str) { return null; } /** * @param {Format | FormatInterface} format * @param {RemoveFirstElement>} args * @returns {Format} */ static get (format, ...args) { if (!format || isInstance(format, this)) { return /** @type {Format} */ (format); } if (format[instance]) { return format[instance]; } return new Format(format, ...args); } } /** * A class for adding deep extensibility to any piece of JS code */ class Hooks { add (name, callback, first) { if (typeof arguments[0] != "string") { // Multiple hooks for (var name in arguments[0]) { this.add(name, arguments[0][name], arguments[1]); } return; } (Array.isArray(name) ? name : [name]).forEach(function (name) { this[name] = this[name] || []; if (callback) { this[name][first ? "unshift" : "push"](callback); } }, this); } run (name, env) { this[name] = this[name] || []; this[name].forEach(function (callback) { callback.call(env && env.context ? env.context : env, env); }); } } /** * The instance of {@link Hooks} used throughout Color.js */ const hooks = new Hooks(); // Type re-exports /** @typedef {import("./types.js").White} White */ /** @type {Record} */ // prettier-ignore const WHITES = { // for compatibility, the four-digit chromaticity-derived ones everyone else uses D50: [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585], D65: [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290], }; /** * * @param {string | White} name * @returns {White} */ function getWhite (name) { if (Array.isArray(name)) { return name; } return WHITES[name]; } /** * Adapt XYZ from white point W1 to W2 * @param {White | string} W1 * @param {White | string} W2 * @param {[number, number, number]} XYZ * @param {{ method?: string | undefined }} options * @returns {[number, number, number]} */ function adapt$1 (W1, W2, XYZ, options = {}) { W1 = getWhite(W1); W2 = getWhite(W2); if (!W1 || !W2) { throw new TypeError( `Missing white point to convert ${!W1 ? "from" : ""}${!W1 && !W2 ? "/" : ""}${!W2 ? "to" : ""}`, ); } if (W1 === W2) { // Same whitepoints, no conversion needed return XYZ; } let env = { W1, W2, XYZ, options }; hooks.run("chromatic-adaptation-start", env); if (!env.M) { if (env.W1 === WHITES.D65 && env.W2 === WHITES.D50) { // prettier-ignore env.M = [ [ 1.0479297925449969, 0.022946870601609652, -0.05019226628920524 ], [ 0.02962780877005599, 0.9904344267538799, -0.017073799063418826 ], [ -0.009243040646204504, 0.015055191490298152, 0.7518742814281371 ], ]; } else if (env.W1 === WHITES.D50 && env.W2 === WHITES.D65) { // prettier-ignore env.M = [ [ 0.955473421488075, -0.02309845494876471, 0.06325924320057072 ], [ -0.0283697093338637, 1.0099953980813041, 0.021041441191917323 ], [ 0.012314014864481998, -0.020507649298898964, 1.330365926242124 ], ]; } } hooks.run("chromatic-adaptation-end", env); if (env.M) { return multiply_v3_m3x3(env.XYZ, env.M); } else { throw new TypeError("Only Bradford CAT with white points D50 and D65 supported for now."); } } // Global defaults one may want to configure var defaults$1 = { gamut_mapping: "css", precision: 5, deltaE: "76", // Default deltaE method verbose: globalThis?.process?.env?.NODE_ENV?.toLowerCase() !== "test", warn: function warn (msg) { if (this.verbose) { globalThis?.console?.warn?.(msg); } }, }; /** @import { ColorConstructor } from "./types.js" */ // Type re-exports /** @typedef {import("./types.js").ArgumentMeta} ArgumentMeta */ /** @typedef {import("./types.js").ParseFunctionReturn} ParseFunctionReturn */ /** @typedef {import("./types.js").ParseOptions} ParseOptions */ /** * Convert a CSS Color string to a color object * @param {string} str * @param {ParseOptions} [options] * @returns {ColorConstructor} */ function parse$4 (str, options) { let env = { str: String(str)?.trim(), options, }; hooks.run("parse-start", env); if (env.color) { return env.color; } env.parsed = parseFunction(env.str); let ret; let meta = env.options ? (env.options.parseMeta ?? env.options.meta) : null; if (env.parsed) { // Is a functional syntax let name = env.parsed.name; let format; let space; let coords = env.parsed.args; let types = coords.map((c, i) => env.parsed.argMeta[i]?.type); if (name === "color") { // color() function let id = coords.shift(); types.shift(); // Check against both and versions let alternateId = id.startsWith("--") ? id.substring(2) : `--${id}`; let ids = [id, alternateId]; format = ColorSpace.findFormat({ name, id: ids, type: "function" }); if (!format) { // Not found let didYouMean; let registryId = id in ColorSpace.registry ? id : alternateId; if (registryId in ColorSpace.registry) { // Used color space id instead of color() id, these are often different let cssId = ColorSpace.registry[registryId].formats?.color?.id; if (cssId) { let altColor = str.replace("color(" + id, "color(" + cssId); didYouMean = `Did you mean ${altColor}?`; } } throw new TypeError( `Cannot parse ${env.str}. ` + (didYouMean ?? "Missing a plugin?"), ); } space = format.space; if (format.id.startsWith("--") && !id.startsWith("--")) { defaults$1.warn( `${space.name} is a non-standard space and not currently supported in the CSS spec. ` + `Use prefixed color(${format.id}) instead of color(${id}).`, ); } if (id.startsWith("--") && !format.id.startsWith("--")) { defaults$1.warn( `${space.name} is a standard space and supported in the CSS spec. ` + `Use color(${format.id}) instead of prefixed color(${id}).`, ); } } else { format = ColorSpace.findFormat({ name, type: "function" }); space = format.space; } if (meta) { Object.assign(meta, { format, formatId: format.name, types, commas: env.parsed.commas, }); } let alpha = 1; if (env.parsed.lastAlpha) { alpha = env.parsed.args.pop(); if (meta) { meta.alphaType = types.pop(); } } let coordCount = format.coords.length; if (coords.length !== coordCount) { throw new TypeError( `Expected ${coordCount} coordinates for ${space.id} in ${env.str}), got ${coords.length}`, ); } coords = format.coerceCoords(coords, types); ret = { spaceId: space.id, coords, alpha }; } else { // Custom, colorspace-specific format spaceloop: for (let space of ColorSpace.all) { for (let formatId in space.formats) { let format = space.formats[formatId]; if (format.type !== "custom") { continue; } if (format.test && !format.test(env.str)) { continue; } // Convert to Format object let formatObject = space.getFormat(format); let color = formatObject.parse(env.str); if (color) { if (meta) { Object.assign(meta, { format: formatObject, formatId }); } ret = color; break spaceloop; } } } } if (!ret) { // If we're here, we couldn't parse throw new TypeError(`Could not parse ${str} as a color. Missing a plugin?`); } // Clamp alpha to [0, 1] ret.alpha = isNone(ret.alpha) ? ret.alpha : ret.alpha === undefined ? 1 : clamp(0, ret.alpha, 1); return ret; } /** * Units and multiplication factors for the internally stored numbers */ const units = { "%": 0.01, deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360, }; const regex = { // Need to list calc(NaN) explicitly as otherwise its ending paren would terminate the function call function: /^([a-z]+)\(((?:calc\(NaN\)|.)+?)\)$/i, number: /^([-+]?(?:[0-9]*\.)?[0-9]+(e[-+]?[0-9]+)?)$/i, unitValue: RegExp(`(${Object.keys(units).join("|")})$`), // NOTE The -+ are not just for prefix, but also for idents, and e+N notation! singleArgument: /\/?\s*(none|NaN|calc\(NaN\)|[-+\w.]+(?:%|deg|g?rad|turn)?)/g, }; /** * Parse a single function argument * @param {string} rawArg * @returns {{value: number, meta: ArgumentMeta}} */ function parseArgument (rawArg) { /** @type {Partial} */ let meta = {}; let unit = rawArg.match(regex.unitValue)?.[0]; /** @type {string | number} */ let value = (meta.raw = rawArg); if (unit) { // It’s a dimension token meta.type = unit === "%" ? "" : ""; meta.unit = unit; meta.unitless = Number(value.slice(0, -unit.length)); // unitless number value = meta.unitless * units[unit]; } else if (regex.number.test(value)) { // It's a number // Convert numerical args to numbers value = Number(value); meta.type = ""; } else if (value === "none") { value = null; } else if (value === "NaN" || value === "calc(NaN)") { value = NaN; meta.type = ""; } else { meta.type = ""; } return { value: /** @type {number} */ (value), meta: /** @type {ArgumentMeta} */ (meta) }; } /** * Parse a CSS function, regardless of its name and arguments * @param {string} str String to parse * @return {ParseFunctionReturn | void} */ function parseFunction (str) { if (!str) { return; } str = str.trim(); let parts = str.match(regex.function); if (parts) { // It is a function, parse args let args = []; let argMeta = []; let lastAlpha = false; let name = parts[1].toLowerCase(); let separators = parts[2].replace(regex.singleArgument, ($0, rawArg) => { let { value, meta } = parseArgument(rawArg); if ( // If there's a slash here, it's modern syntax $0.startsWith("/") || // If there's still elements to process after there's already 3 in `args` (and the we're not dealing with "color()"), it's likely to be a legacy color like "hsl(0, 0%, 0%, 0.5)" (name !== "color" && args.length === 3) ) { // It's alpha lastAlpha = true; } args.push(value); argMeta.push(meta); return ""; }); return { name, args, argMeta, lastAlpha, commas: separators.includes(","), rawName: parts[1], rawArgs: parts[2], }; } } /** @import { ColorTypes, ParseOptions as GetColorOptions, PlainColorObject } from "./types.js" */ /** * Resolves a color reference (object or string) to a plain color object * @overload * @param {ColorTypes} color * @param {GetColorOptions} [options] * @returns {PlainColorObject} */ /** * @overload * @param {ColorTypes[]} color * @param {GetColorOptions} [options] * @returns {PlainColorObject[]} */ function getColor (color, options) { if (Array.isArray(color)) { return color.map(c => getColor(c, options)); } if (!color) { throw new TypeError("Empty color reference"); } if (isString(color)) { color = parse$4(color, options); } // Object fixup let space = color.space || color.spaceId; if (typeof space === "string") { // Convert string id to color space object color.space = ColorSpace.get(space); } if (color.alpha === undefined) { color.alpha = 1; } return color; } /** * @packageDocumentation * Defines the class and other types related to creating color spaces. * For the builtin color spaces, see the `spaces` module. */ const ε$3 = 0.000075; /** * Class to represent a color space */ class ColorSpace { constructor (options) { this.id = options.id; this.name = options.name; this.base = options.base ? ColorSpace.get(options.base) : null; this.aliases = options.aliases; if (this.base) { this.fromBase = options.fromBase; this.toBase = options.toBase; } // Coordinate metadata let coords = options.coords ?? this.base.coords; for (let name in coords) { if (!("name" in coords[name])) { coords[name].name = name; } } this.coords = coords; // White point let white = options.white ?? this.base.white ?? "D65"; this.white = getWhite(white); // Sort out formats this.formats = options.formats ?? {}; for (let name in this.formats) { let format = this.formats[name]; format.type ||= "function"; format.name ||= name; } if (!this.formats.color?.id) { this.formats.color = { ...(this.formats.color ?? {}), id: options.cssId || this.id, }; } // Gamut space if (options.gamutSpace) { // Gamut space explicitly specified this.gamutSpace = options.gamutSpace === "self" ? this : ColorSpace.get(options.gamutSpace); } else { // No gamut space specified, calculate a sensible default if (this.isPolar) { // Do not check gamut through polar coordinates this.gamutSpace = this.base; } else { this.gamutSpace = this; } } // Optimize inGamut for unbounded spaces if (this.gamutSpace.isUnbounded) { this.inGamut = (coords, options) => { return true; }; } // Other stuff this.referred = options.referred; // Compute ancestors and store them, since they will never change Object.defineProperty(this, "path", { value: getPath$1(this).reverse(), writable: false, enumerable: true, configurable: true, }); hooks.run("colorspace-init-end", this); } inGamut (coords, { epsilon = ε$3 } = {}) { if (!this.equals(this.gamutSpace)) { coords = this.to(this.gamutSpace, coords); return this.gamutSpace.inGamut(coords, { epsilon }); } let coordMeta = Object.values(this.coords); return coords.every((c, i) => { let meta = coordMeta[i]; if (meta.type !== "angle" && meta.range) { if (isNone(c)) { // NaN is always in gamut return true; } let [min, max] = meta.range; return ( (min === undefined || c >= min - epsilon) && (max === undefined || c <= max + epsilon) ); } return true; }); } get isUnbounded () { return Object.values(this.coords).every(coord => !("range" in coord)); } get cssId () { return this.formats?.color?.id || this.id; } get isPolar () { for (let id in this.coords) { if (this.coords[id].type === "angle") { return true; } } return false; } /** * Lookup a format in this color space * @param {string | object | Format} format - Format id if string. If object, it's converted to a `Format` object and returned. * @returns {Format} */ getFormat (format) { if (!format) { return null; } if (format === "default") { format = Object.values(this.formats)[0]; } else if (typeof format === "string") { format = this.formats[format]; } let ret = Format.get(format, this); if (ret !== format && format.name in this.formats) { // Update the format we have on file so we can find it more quickly next time this.formats[format.name] = ret; } return ret; } /** * Check if this color space is the same as another color space reference. * Allows proxying color space objects and comparing color spaces with ids. * @param {string | ColorSpace} space ColorSpace object or id to compare to * @returns {boolean} */ equals (space) { if (!space) { return false; } return this === space || this.id === space || this.id === space.id; } to (space, coords) { if (arguments.length === 1) { const color = getColor(space); [space, coords] = [color.space, color.coords]; } space = ColorSpace.get(space); if (this.equals(space)) { // Same space, no change needed return coords; } // Convert NaN to 0, which seems to be valid in every coordinate of every color space coords = coords.map(c => (isNone(c) ? 0 : c)); // Find connection space = lowest common ancestor in the base tree let myPath = this.path; let otherPath = space.path; let connectionSpace, connectionSpaceIndex; for (let i = 0; i < myPath.length; i++) { if (myPath[i].equals(otherPath[i])) { connectionSpace = myPath[i]; connectionSpaceIndex = i; } else { break; } } if (!connectionSpace) { // This should never happen throw new Error( `Cannot convert between color spaces ${this} and ${space}: no connection space was found`, ); } // Go up from current space to connection space for (let i = myPath.length - 1; i > connectionSpaceIndex; i--) { coords = myPath[i].toBase(coords); } // Go down from connection space to target space for (let i = connectionSpaceIndex + 1; i < otherPath.length; i++) { coords = otherPath[i].fromBase(coords); } return coords; } from (space, coords) { if (arguments.length === 1) { const color = getColor(space); [space, coords] = [color.space, color.coords]; } space = ColorSpace.get(space); return space.to(this, coords); } toString () { return `${this.name} (${this.id})`; } getMinCoords () { let ret = []; for (let id in this.coords) { let meta = this.coords[id]; let range = meta.range || meta.refRange; ret.push(range?.min ?? 0); } return ret; } static registry = {}; // Returns array of unique color spaces static get all () { return [...new Set(Object.values(ColorSpace.registry))]; } static register (id, space) { if (arguments.length === 1) { space = arguments[0]; id = space.id; } space = this.get(space); if (this.registry[id] && this.registry[id] !== space) { throw new Error(`Duplicate color space registration: '${id}'`); } this.registry[id] = space; // Register aliases when called without an explicit ID. if (arguments.length === 1 && space.aliases) { for (let alias of space.aliases) { this.register(alias, space); } } return space; } /** * Lookup ColorSpace object by name * @param {ColorSpace | string} name */ static get (space, ...alternatives) { if (!space || isInstance(space, this)) { return space; } let argType = type$1(space); if (argType === "string") { // It's a color space id let ret = ColorSpace.registry[space.toLowerCase()]; if (!ret) { throw new TypeError(`No color space found with id = "${space}"`); } return ret; } if (alternatives.length) { return ColorSpace.get(...alternatives); } throw new TypeError(`${space} is not a valid color space`); } /** * Look up all color spaces for a format that matches certain criteria * @param {object | string} filters * @param {Array} [spaces=ColorSpace.all] * @returns {Format | null} */ static findFormat (filters, spaces = ColorSpace.all) { if (!filters) { return null; } if (typeof filters === "string") { filters = { name: filters }; } for (let space of spaces) { for (let [name, format] of Object.entries(space.formats)) { format.name ??= name; format.type ??= "function"; let matches = (!filters.name || format.name === filters.name) && (!filters.type || format.type === filters.type); if (filters.id) { let ids = format.ids || [format.id]; let filterIds = Array.isArray(filters.id) ? filters.id : [filters.id]; matches &&= filterIds.some(id => ids.includes(id)); } if (matches) { let ret = Format.get(format, space); if (ret !== format) { space.formats[format.name] = ret; } return ret; } } } return null; } /** * Get metadata about a coordinate of a color space * * @static * @param {Array | string} ref * @param {ColorSpace | string} [workingSpace] * @return {Object} */ static resolveCoord (ref, workingSpace) { let coordType = type$1(ref); let space, coord; if (coordType === "string") { if (ref.includes(".")) { // Absolute coordinate [space, coord] = ref.split("."); } else { // Relative coordinate [space, coord] = [, ref]; } } else if (Array.isArray(ref)) { [space, coord] = ref; } else { // Object space = ref.space; coord = ref.coordId; } space = ColorSpace.get(space); if (!space) { space = workingSpace; } if (!space) { throw new TypeError( `Cannot resolve coordinate reference ${ref}: No color space specified and relative references are not allowed here`, ); } coordType = type$1(coord); if (coordType === "number" || (coordType === "string" && coord >= 0)) { // Resolve numerical coord let meta = Object.entries(space.coords)[coord]; if (meta) { return { space, id: meta[0], index: coord, ...meta[1] }; } } space = ColorSpace.get(space); let normalizedCoord = coord.toLowerCase(); let i = 0; for (let id in space.coords) { let meta = space.coords[id]; if ( id.toLowerCase() === normalizedCoord || meta.name?.toLowerCase() === normalizedCoord ) { return { space, id, index: i, ...meta }; } i++; } throw new TypeError( `No "${coord}" coordinate found in ${space.name}. Its coordinates are: ${Object.keys(space.coords).join(", ")}`, ); } static DEFAULT_FORMAT = { type: "functions", name: "color", }; } function getPath$1 (space) { let ret = [space]; for (let s = space; (s = s.base); ) { ret.push(s); } return ret; } var xyz_d65 = new ColorSpace({ id: "xyz-d65", name: "XYZ D65", coords: { x: { refRange: [0, 1], name: "X", }, y: { refRange: [0, 1], name: "Y", }, z: { refRange: [0, 1], name: "Z", }, }, white: "D65", formats: { color: { ids: ["xyz-d65", "xyz"], }, }, aliases: ["xyz"], }); // Type re-exports /** @typedef {import("./types.js").RGBOptions} RGBOptions */ /** Convenience class for RGB color spaces */ class RGBColorSpace extends ColorSpace { /** * Creates a new RGB ColorSpace. * If coords are not specified, they will use the default RGB coords. * Instead of `fromBase()` and `toBase()` functions, * you can specify to/from XYZ matrices and have `toBase()` and `fromBase()` automatically generated. * @param {RGBOptions} options */ constructor (options) { if (!options.coords) { options.coords = { r: { range: [0, 1], name: "Red", }, g: { range: [0, 1], name: "Green", }, b: { range: [0, 1], name: "Blue", }, }; } if (!options.base) { options.base = xyz_d65; } if (options.toXYZ_M && options.fromXYZ_M) { options.toBase ??= rgb => { let xyz = multiply_v3_m3x3(rgb, options.toXYZ_M); if (this.white !== this.base.white) { // Perform chromatic adaptation xyz = adapt$1(this.white, this.base.white, xyz); } return xyz; }; options.fromBase ??= xyz => { xyz = adapt$1(this.base.white, this.white, xyz); return multiply_v3_m3x3(xyz, options.fromXYZ_M); }; } options.referred ??= "display"; super(options); } } /** @import { ColorTypes, Coords } from "./types.js" */ /** * Options for {@link getAll} * @typedef GetAllOptions * @property {string | ColorSpace | undefined} [space] * The color space to convert to. Defaults to the color's current space * @property {number | undefined} [precision] * The number of significant digits to round the coordinates to */ /** * Get the coordinates of a color in any color space * @overload * @param {ColorTypes} color * @param {string | ColorSpace} [options=color.space] The color space to convert to. Defaults to the color's current space * @returns {Coords} The color coordinates in the given color space */ /** * @overload * @param {ColorTypes} color * @param {GetAllOptions} [options] * @returns {Coords} The color coordinates in the given color space */ function getAll (color, options) { color = getColor(color); let space = ColorSpace.get(options, options?.space); let precision = options?.precision; let coords; if (!space || color.space.equals(space)) { // No conversion needed coords = color.coords.slice(); } else { coords = space.from(color); } return precision === undefined ? coords : coords.map(coord => toPrecision(coord, precision)); } /** @import { ColorTypes, Ref } from "./types.js" */ /** * @param {ColorTypes} color * @param {Ref} prop * @returns {number} */ function get$1 (color, prop) { color = getColor(color); if (prop === "alpha") { return color.alpha ?? 1; } let { space, index } = ColorSpace.resolveCoord(prop, color.space); let coords = getAll(color, space); return coords[index]; } /** @import { ColorTypes, Coords, PlainColorObject } from "./types.js" */ /** * Set all coordinates of a color at once, in its own color space or another. * Modifies the color in place. * @overload * @param {ColorTypes} color * @param {Coords} coords Array of coordinates * @param {number} [alpha] * @returns {PlainColorObject} */ /** * @overload * @param {ColorTypes} color * @param {string | ColorSpace} space The color space of the provided coordinates. * @param {Coords} coords Array of coordinates * @param {number} [alpha] * @returns {PlainColorObject} */ function setAll (color, space, coords, alpha) { color = getColor(color); if (Array.isArray(space)) { // Space is omitted [space, coords, alpha] = [color.space, space, coords]; } space = ColorSpace.get(space); // Make sure we have a ColorSpace object color.coords = space === color.space ? coords.slice() : space.to(color.space, coords); if (alpha !== undefined) { color.alpha = alpha; } return color; } /** @type {"color"} */ setAll.returns = "color"; /** @import { ColorTypes, PlainColorObject, Ref } from "./types.js" */ /** * Set properties and return current instance * @overload * @param {ColorTypes} color * @param {Ref} prop * @param {number | ((coord: number) => number)} value * @returns {PlainColorObject} */ /** * @overload * @param {ColorTypes} color * @param {Record number)>} props * @returns {PlainColorObject} */ function set (color, prop, value) { color = getColor(color); if (arguments.length === 2 && type$1(arguments[1]) === "object") { // Argument is an object literal let object = arguments[1]; for (let p in object) { set(color, p, object[p]); } } else { if (typeof value === "function") { value = value(get$1(color, prop)); } if (prop === "alpha") { color.alpha = value; } else { let { space, index } = ColorSpace.resolveCoord(prop, color.space); let coords = getAll(color, space); coords[index] = value; setAll(color, space, coords); } } return color; } /** @type {"color"} */ set.returns = "color"; var XYZ_D50 = new ColorSpace({ id: "xyz-d50", name: "XYZ D50", white: "D50", base: xyz_d65, fromBase: coords => adapt$1(xyz_d65.white, "D50", coords), toBase: coords => adapt$1("D50", xyz_d65.white, coords), }); // κ * ε = 2^3 = 8 const ε$2 = 216 / 24389; // 6^3/29^3 == (24/116)^3 const ε3 = 24 / 116; const κ$1 = 24389 / 27; // 29^3/3^3 let white$2 = WHITES.D50; var Lab = new ColorSpace({ id: "lab", name: "Lab", coords: { l: { refRange: [0, 100], name: "Lightness", }, a: { refRange: [-125, 125], }, b: { refRange: [-125, 125], }, }, // Assuming XYZ is relative to D50, convert to CIE Lab // from CIE standard, which now defines these as a rational fraction white: white$2, base: XYZ_D50, // Convert D50-adapted XYX to Lab // CIE 15.3:2004 section 8.2.1.1 fromBase (XYZ) { // XYZ scaled relative to reference white let xyz = XYZ.map((value, i) => value / white$2[i]); let f = xyz.map(value => (value > ε$2 ? Math.cbrt(value) : (κ$1 * value + 16) / 116)); let L = 116 * f[1] - 16; let a = 500 * (f[0] - f[1]); let b = 200 * (f[1] - f[2]); return [L, a, b]; }, // Convert Lab to D50-adapted XYZ // Same result as CIE 15.3:2004 Appendix D although the derivation is different // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html toBase (Lab) { // compute f, starting with the luminance-related term let [L, a, b] = Lab; let f = []; f[1] = (L + 16) / 116; f[0] = a / 500 + f[1]; f[2] = f[1] - b / 200; // compute xyz // prettier-ignore let xyz = [ f[0] > ε3 ? Math.pow(f[0], 3) : (116 * f[0] - 16) / κ$1, Lab[0] > 8 ? Math.pow((Lab[0] + 16) / 116, 3) : Lab[0] / κ$1, f[2] > ε3 ? Math.pow(f[2], 3) : (116 * f[2] - 16) / κ$1, ]; // Compute XYZ by scaling xyz by reference white return xyz.map((value, i) => value * white$2[i]); }, formats: { lab: { coords: [ " | ", " | ", " | ", ], }, }, }); /** * Constrain an angle to 360 degrees * @param {number} angle * @returns {number} */ function constrain$1 (angle) { if (typeof angle !== "number") { return angle; } return ((angle % 360) + 360) % 360; } /** * @param {"raw" | "increasing" | "decreasing" | "longer" | "shorter"} arc * @param {[number, number]} angles * @returns {[number, number]} */ function adjust (arc, angles) { let [a1, a2] = angles; let none1 = isNone(a1); let none2 = isNone(a2); if (none1 && none2) { return [a1, a2]; } else if (none1) { a1 = a2; } else if (none2) { a2 = a1; } if (arc === "raw") { return angles; } a1 = constrain$1(a1); a2 = constrain$1(a2); let angleDiff = a2 - a1; if (arc === "increasing") { if (angleDiff < 0) { a2 += 360; } } else if (arc === "decreasing") { if (angleDiff > 0) { a1 += 360; } } else if (arc === "longer") { if (-180 < angleDiff && angleDiff < 180) { if (angleDiff > 0) { a1 += 360; } else { a2 += 360; } } } else if (arc === "shorter") { if (angleDiff > 180) { a1 += 360; } else if (angleDiff < -180) { a2 += 360; } } return [a1, a2]; } var LCHSpace = new ColorSpace({ id: "lch", name: "LCH", coords: { l: { refRange: [0, 100], name: "Lightness", }, c: { refRange: [0, 150], name: "Chroma", }, h: { refRange: [0, 360], type: "angle", name: "Hue", }, }, base: Lab, fromBase (Lab) { // These methods are used for other polar forms as well, so we can't hardcode the ε if (this.ε === undefined) { // @ts-expect-error Property 'coords' does not exist on type 'string | ColorSpace' let range = Object.values(this.base.coords)[1].refRange; let extent = range[1] - range[0]; this.ε = extent / 100000; } // Convert to polar form let [L, a, b] = Lab; let isAchromatic = Math.abs(a) < this.ε && Math.abs(b) < this.ε; let h = isAchromatic ? null : constrain$1((Math.atan2(b, a) * 180) / Math.PI); let C = isAchromatic ? 0 : Math.sqrt(a ** 2 + b ** 2); return [L, C, h]; }, toBase (lch) { // Convert from polar form let [L, C, h] = lch; let a = null, b = null; if (!isNone(h)) { C = C < 0 ? 0 : C; // Clamp negative Chroma a = C * Math.cos((h * Math.PI) / 180); b = C * Math.sin((h * Math.PI) / 180); } return [L, a, b]; }, formats: { lch: { coords: [" | ", " | ", " | "], }, }, }); // deltaE2000 is a statistically significant improvement // and is recommended by the CIE and Idealliance // especially for color differences less than 10 deltaE76 // but is wicked complicated // and many implementations have small errors! // DeltaE2000 is also discontinuous; in case this // matters to you, use deltaECMC instead. const Gfactor = 25 ** 7; const π$1 = Math.PI; const r2d = 180 / π$1; const d2r$1 = π$1 / 180; function pow7 (x) { // Faster than x ** 7 or Math.pow(x, 7) const x2 = x * x; const x7 = x2 * x2 * x2 * x; return x7; } /** * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @param {{ kL?: number | undefined; kC?: number | undefined; kH?: number | undefined }} options * @returns {number} */ function deltaE2000 (color, sample, { kL = 1, kC = 1, kH = 1 } = {}) { [color, sample] = getColor([color, sample]); // Given this color as the reference // and the function parameter as the sample, // calculate deltaE 2000. // This implementation assumes the parametric // weighting factors kL, kC and kH // for the influence of viewing conditions // are all 1, as sadly seems typical. // kL should be increased for lightness texture or noise // and kC increased for chroma noise let [L1, a1, b1] = Lab.from(color); let C1 = LCHSpace.from(Lab, [L1, a1, b1])[1]; let [L2, a2, b2] = Lab.from(sample); let C2 = LCHSpace.from(Lab, [L2, a2, b2])[1]; // Check for negative Chroma, // which might happen through // direct user input of LCH values if (C1 < 0) { C1 = 0; } if (C2 < 0) { C2 = 0; } let Cbar = (C1 + C2) / 2; // mean Chroma // calculate a-axis asymmetry factor from mean Chroma // this turns JND ellipses for near-neutral colors back into circles let C7 = pow7(Cbar); let G = 0.5 * (1 - Math.sqrt(C7 / (C7 + Gfactor))); // scale a axes by asymmetry factor // this by the way is why there is no Lab2000 colorspace let adash1 = (1 + G) * a1; let adash2 = (1 + G) * a2; // calculate new Chroma from scaled a and original b axes let Cdash1 = Math.sqrt(adash1 ** 2 + b1 ** 2); let Cdash2 = Math.sqrt(adash2 ** 2 + b2 ** 2); // calculate new hues, with zero hue for true neutrals // and in degrees, not radians let h1 = adash1 === 0 && b1 === 0 ? 0 : Math.atan2(b1, adash1); let h2 = adash2 === 0 && b2 === 0 ? 0 : Math.atan2(b2, adash2); if (h1 < 0) { h1 += 2 * π$1; } if (h2 < 0) { h2 += 2 * π$1; } h1 *= r2d; h2 *= r2d; // Lightness and Chroma differences; sign matters let ΔL = L2 - L1; let ΔC = Cdash2 - Cdash1; // Hue difference, getting the sign correct let hdiff = h2 - h1; let hsum = h1 + h2; let habs = Math.abs(hdiff); let Δh; if (Cdash1 * Cdash2 === 0) { Δh = 0; } else if (habs <= 180) { Δh = hdiff; } else if (hdiff > 180) { Δh = hdiff - 360; } else if (hdiff < -180) { Δh = hdiff + 360; } else { defaults$1.warn("the unthinkable has happened"); } // weighted Hue difference, more for larger Chroma let ΔH = 2 * Math.sqrt(Cdash2 * Cdash1) * Math.sin((Δh * d2r$1) / 2); // calculate mean Lightness and Chroma let Ldash = (L1 + L2) / 2; let Cdash = (Cdash1 + Cdash2) / 2; let Cdash7 = pow7(Cdash); // Compensate for non-linearity in the blue region of Lab. // Four possibilities for hue weighting factor, // depending on the angles, to get the correct sign let hdash; if (Cdash1 * Cdash2 === 0) { hdash = hsum; // which should be zero } else if (habs <= 180) { hdash = hsum / 2; } else if (hsum < 360) { hdash = (hsum + 360) / 2; } else { hdash = (hsum - 360) / 2; } // positional corrections to the lack of uniformity of CIELAB // These are all trying to make JND ellipsoids more like spheres // SL Lightness crispening factor // a background with L=50 is assumed let lsq = (Ldash - 50) ** 2; let SL = 1 + (0.015 * lsq) / Math.sqrt(20 + lsq); // SC Chroma factor, similar to those in CMC and deltaE 94 formulae let SC = 1 + 0.045 * Cdash; // Cross term T for blue non-linearity let T = 1; T -= 0.17 * Math.cos((hdash - 30) * d2r$1); T += 0.24 * Math.cos(2 * hdash * d2r$1); T += 0.32 * Math.cos((3 * hdash + 6) * d2r$1); T -= 0.2 * Math.cos((4 * hdash - 63) * d2r$1); // SH Hue factor depends on Chroma, // as well as adjusted hue angle like deltaE94. let SH = 1 + 0.015 * Cdash * T; // RT Hue rotation term compensates for rotation of JND ellipses // and Munsell constant hue lines // in the medium-high Chroma blue region // (Hue 225 to 315) let Δθ = 30 * Math.exp(-1 * ((hdash - 275) / 25) ** 2); let RC = 2 * Math.sqrt(Cdash7 / (Cdash7 + Gfactor)); let RT = -1 * Math.sin(2 * Δθ * d2r$1) * RC; // Finally calculate the deltaE, term by term as root sume of squares let dE = (ΔL / (kL * SL)) ** 2; dE += (ΔC / (kC * SC)) ** 2; dE += (ΔH / (kH * SH)) ** 2; dE += RT * (ΔC / (kC * SC)) * (ΔH / (kH * SH)); return Math.sqrt(dE); // Yay!!! } /** @import { Matrix3x3 } from "../types.js" */ // Recalculated for consistent reference white // see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-943521484 /** @type {Matrix3x3} */ // prettier-ignore const XYZtoLMS_M$1 = [ [ 0.8190224379967030, 0.3619062600528904, -0.1288737815209879 ], [ 0.0329836539323885, 0.9292868615863434, 0.0361446663506424 ], [ 0.0481771893596242, 0.2642395317527308, 0.6335478284694309 ], ]; // inverse of XYZtoLMS_M /** @type {Matrix3x3} */ // prettier-ignore const LMStoXYZ_M$1 = [ [ 1.2268798758459243, -0.5578149944602171, 0.2813910456659647 ], [ -0.0405757452148008, 1.1122868032803170, -0.0717110580655164 ], [ -0.0763729366746601, -0.4214933324022432, 1.5869240198367816 ], ]; /** @type {Matrix3x3} */ // prettier-ignore const LMStoLab_M = [ [ 0.2104542683093140, 0.7936177747023054, -0.0040720430116193 ], [ 1.9779985324311684, -2.42859224204858, 0.4505937096174110 ], [ 0.0259040424655478, 0.7827717124575296, -0.8086757549230774 ], ]; // LMStoIab_M inverted /** @type {Matrix3x3} */ // prettier-ignore const LabtoLMS_M = [ [ 1.0000000000000000, 0.3963377773761749, 0.2158037573099136 ], [ 1.0000000000000000, -0.1055613458156586, -0.0638541728258133 ], [ 1.0000000000000000, -0.0894841775298119, -1.2914855480194092 ], ]; var OKLab = new ColorSpace({ id: "oklab", name: "Oklab", coords: { l: { refRange: [0, 1], name: "Lightness", }, a: { refRange: [-0.4, 0.4], }, b: { refRange: [-0.4, 0.4], }, }, // Note that XYZ is relative to D65 white: "D65", base: xyz_d65, fromBase (XYZ) { // move to LMS cone domain let LMS = multiply_v3_m3x3(XYZ, XYZtoLMS_M$1); // non-linearity LMS[0] = Math.cbrt(LMS[0]); LMS[1] = Math.cbrt(LMS[1]); LMS[2] = Math.cbrt(LMS[2]); return multiply_v3_m3x3(LMS, LMStoLab_M, LMS); }, toBase (OKLab) { // move to LMS cone domain let LMSg = multiply_v3_m3x3(OKLab, LabtoLMS_M); // restore linearity LMSg[0] = LMSg[0] ** 3; LMSg[1] = LMSg[1] ** 3; LMSg[2] = LMSg[2] ** 3; return multiply_v3_m3x3(LMSg, LMStoXYZ_M$1, LMSg); }, formats: { oklab: { coords: [ " | ", " | ", " | ", ], }, }, }); /** * More accurate color-difference formulae * than the simple 1976 Euclidean distance in CIE Lab * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @returns {number} */ function deltaEOK (color, sample) { [color, sample] = getColor([color, sample]); // Given this color as the reference // and a sample, // calculate deltaEOK, term by term as root sum of squares let [L1, a1, b1] = OKLab.from(color); let [L2, a2, b2] = OKLab.from(sample); let ΔL = L1 - L2; let Δa = a1 - a2; let Δb = b1 - b2; return Math.sqrt(ΔL ** 2 + Δa ** 2 + Δb ** 2); } /** @import { ColorTypes } from "./types.js" */ const ε$1 = 0.000075; /** * Check if a color is in gamut of either its own or another color space * @param {ColorTypes} color * @param {string | ColorSpace} [space] * @param {{ epsilon?: number | undefined }} [param2] * @returns {boolean} */ function inGamut (color, space, { epsilon = ε$1 } = {}) { color = getColor(color); if (!space) { space = color.space; } space = ColorSpace.get(space); let coords = color.coords; if (space !== color.space) { coords = space.from(color); } return space.inGamut(coords, { epsilon }); } /** @import { Coords, PlainColorObject } from "./types.js" */ /** * @param {PlainColorObject} color * @returns {PlainColorObject} */ function clone$1 (color) { return { space: color.space, coords: /** @type {Coords} */ (color.coords.slice()), alpha: color.alpha, }; } /** @import { ColorTypes } from "./types.js" */ /** * Euclidean distance of colors in an arbitrary color space * @param {ColorTypes} color1 * @param {ColorTypes} color2 * @param {string | ColorSpace} space * @returns {number} */ function distance (color1, color2, space = "lab") { space = ColorSpace.get(space); // Assume getColor() is called on color in space.from() let coords1 = space.from(color1); let coords2 = space.from(color2); return Math.sqrt( coords1.reduce((acc, c1, i) => { let c2 = coords2[i]; if (isNone(c1) || isNone(c2)) { return acc; } return acc + (c2 - c1) ** 2; }, 0), ); } /** * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @returns {number} */ function deltaE76 (color, sample) { // Assume getColor() is called in the distance function return distance(color, sample, "lab"); } // More accurate color-difference formulae // than the simple 1976 Euclidean distance in Lab // CMC by the Color Measurement Committee of the // Bradford Society of Dyeists and Colorsts, 1994. // Uses LCH rather than Lab, // with different weights for L, C and H differences // A nice increase in accuracy for modest increase in complexity const π = Math.PI; const d2r = π / 180; /** * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @param {{ l?: number | undefined; c?: number | undefined }} options * @returns {number} */ function deltaECMC (color, sample, { l = 2, c = 1 } = {}) { [color, sample] = getColor([color, sample]); // Given this color as the reference // and a sample, // calculate deltaE CMC. // This implementation assumes the parametric // weighting factors l:c are 2:1 // which is typical for non-textile uses. let [L1, a1, b1] = Lab.from(color); let [, C1, H1] = LCHSpace.from(Lab, [L1, a1, b1]); let [L2, a2, b2] = Lab.from(sample); let C2 = LCHSpace.from(Lab, [L2, a2, b2])[1]; // let [L1, a1, b1] = color.getAll(lab); // let C1 = color.get("lch.c"); // let H1 = color.get("lch.h"); // let [L2, a2, b2] = sample.getAll(lab); // let C2 = sample.get("lch.c"); // Check for negative Chroma, // which might happen through // direct user input of LCH values if (C1 < 0) { C1 = 0; } if (C2 < 0) { C2 = 0; } // we don't need H2 as ΔH is calculated from Δa, Δb and ΔC // Lightness and Chroma differences // These are (color - sample), unlike deltaE2000 let ΔL = L1 - L2; let ΔC = C1 - C2; let Δa = a1 - a2; let Δb = b1 - b2; // weighted Hue difference, less for larger Chroma difference let H2 = Δa ** 2 + Δb ** 2 - ΔC ** 2; // due to roundoff error it is possible that, for zero a and b, // ΔC > Δa + Δb is 0, resulting in attempting // to take the square root of a negative number // trying instead the equation from Industrial Color Physics // By Georg A. Klein // let ΔH = ((a1 * b2) - (a2 * b1)) / Math.sqrt(0.5 * ((C2 * C1) + (a2 * a1) + (b2 * b1))); // console.log({ΔH}); // This gives the same result to 12 decimal places // except it sometimes NaNs when trying to root a negative number // let ΔH = Math.sqrt(H2); we never actually use the root, it gets squared again!! // positional corrections to the lack of uniformity of CIELAB // These are all trying to make JND ellipsoids more like spheres // SL Lightness crispening factor, depends entirely on L1 not L2 let SL = 0.511; // linear portion of the Y to L transfer function if (L1 >= 16) { // cubic portion SL = (0.040975 * L1) / (1 + 0.01765 * L1); } // SC Chroma factor let SC = (0.0638 * C1) / (1 + 0.0131 * C1) + 0.638; // Cross term T for blue non-linearity let T; if (isNone(H1)) { H1 = 0; } if (H1 >= 164 && H1 <= 345) { T = 0.56 + Math.abs(0.2 * Math.cos((H1 + 168) * d2r)); } else { T = 0.36 + Math.abs(0.4 * Math.cos((H1 + 35) * d2r)); } // console.log({T}); // SH Hue factor also depends on C1, let C4 = Math.pow(C1, 4); let F = Math.sqrt(C4 / (C4 + 1900)); let SH = SC * (F * T + 1 - F); // Finally calculate the deltaE, term by term as root sume of squares let dE = (ΔL / (l * SL)) ** 2; dE += (ΔC / (c * SC)) ** 2; dE += H2 / SH ** 2; // dE += (ΔH / SH) ** 2; return Math.sqrt(dE); // Yay!!! } const Yw = 203; // absolute luminance of media white var XYZ_Abs_D65 = new ColorSpace({ // Absolute CIE XYZ, with a D65 whitepoint, // as used in most HDR colorspaces as a starting point. // SDR spaces are converted per BT.2048 // so that diffuse, media white is 203 cd/m² id: "xyz-abs-d65", cssId: "--xyz-abs-d65", name: "Absolute XYZ D65", coords: { x: { refRange: [0, 9504.7], name: "Xa", }, y: { refRange: [0, 10000], name: "Ya", }, z: { refRange: [0, 10888.3], name: "Za", }, }, base: xyz_d65, fromBase (XYZ) { // Make XYZ absolute, not relative to media white // Maximum luminance in PQ is 10,000 cd/m² // Relative XYZ has Y=1 for media white return XYZ.map(v => v * Yw); }, toBase (AbsXYZ) { // Convert to media-white relative XYZ return AbsXYZ.map(v => v / Yw); }, }); /** @import { Matrix3x3, Vector3 } from "../types.js" */ const b = 1.15; const g = 0.66; const n = 2610 / 2 ** 14; const ninv = 2 ** 14 / 2610; const c1$1 = 3424 / 2 ** 12; const c2$1 = 2413 / 2 ** 7; const c3$1 = 2392 / 2 ** 7; const p = (1.7 * 2523) / 2 ** 5; const pinv = 2 ** 5 / (1.7 * 2523); const d = -0.56; const d0 = 1.6295499532821566e-11; /** @type {Matrix3x3} */ // prettier-ignore const XYZtoCone_M = [ [ 0.41478972, 0.579999, 0.0146480 ], [ -0.20151, 1.120649, 0.0531008 ], [ -0.0166008, 0.264800, 0.6684799 ], ]; // XYZtoCone_M inverted /** @type {Matrix3x3} */ // prettier-ignore const ConetoXYZ_M = [ [ 1.9242264357876067, -1.0047923125953657, 0.037651404030618 ], [ 0.35031676209499907, 0.7264811939316552, -0.06538442294808501 ], [ -0.09098281098284752, -0.3127282905230739, 1.5227665613052603 ], ]; /** @type {Matrix3x3} */ // prettier-ignore const ConetoIab_M = [ [ 0.5, 0.5, 0 ], [ 3.524000, -4.066708, 0.542708 ], [ 0.199076, 1.096799, -1.295875 ], ]; // ConetoIab_M inverted /** @type {Matrix3x3} */ // prettier-ignore const IabtoCone_M = [ [ 1, 0.13860504327153927, 0.05804731615611883 ], [ 1, -0.1386050432715393, -0.058047316156118904 ], [ 1, -0.09601924202631895, -0.811891896056039 ], ]; var Jzazbz = new ColorSpace({ id: "jzazbz", name: "Jzazbz", coords: { jz: { refRange: [0, 1], name: "Jz", }, az: { refRange: [-0.21, 0.21], }, bz: { refRange: [-0.21, 0.21], }, }, base: XYZ_Abs_D65, fromBase (XYZ) { // First make XYZ absolute, not relative to media white // Maximum luminance in PQ is 10,000 cd/m² // Relative XYZ has Y=1 for media white // BT.2048 says media white Y=203 at PQ 58 let [Xa, Ya, Za] = XYZ; // modify X and Y to minimize blue curvature let Xm = b * Xa - (b - 1) * Za; let Ym = g * Ya - (g - 1) * Xa; // move to LMS cone domain let LMS = multiply_v3_m3x3([Xm, Ym, Za], XYZtoCone_M); // PQ-encode LMS let PQLMS = /** @type {Vector3} } */ ( LMS.map(function (val) { let num = c1$1 + c2$1 * spow(val / 10000, n); let denom = 1 + c3$1 * spow(val / 10000, n); return spow(num / denom, p); }) ); // almost there, calculate Iz az bz let [Iz, az, bz] = multiply_v3_m3x3(PQLMS, ConetoIab_M); // console.log({Iz, az, bz}); let Jz = ((1 + d) * Iz) / (1 + d * Iz) - d0; return [Jz, az, bz]; }, toBase (Jzazbz) { let [Jz, az, bz] = Jzazbz; let Iz = (Jz + d0) / (1 + d - d * (Jz + d0)); // bring into LMS cone domain let PQLMS = multiply_v3_m3x3([Iz, az, bz], IabtoCone_M); // convert from PQ-coded to linear-light let LMS = /** @type {Vector3} } */ ( PQLMS.map(function (val) { let num = c1$1 - spow(val, pinv); let denom = c3$1 * spow(val, pinv) - c2$1; let x = 10000 * spow(num / denom, ninv); return x; // luminance relative to diffuse white, [0, 70 or so]. }) ); // modified abs XYZ let [Xm, Ym, Za] = multiply_v3_m3x3(LMS, ConetoXYZ_M); // un-modify X and Y to get D65 XYZ, relative to media white let Xa = (Xm + (b - 1) * Za) / b; let Ya = (Ym + (g - 1) * Xa) / g; return [Xa, Ya, Za]; }, formats: { // https://drafts.csswg.org/css-color-hdr/#Jzazbz jzazbz: { coords: [ " | ", " | ", " | ", ], }, }, }); var jzczhz = new ColorSpace({ id: "jzczhz", name: "JzCzHz", coords: { jz: { refRange: [0, 1], name: "Jz", }, cz: { refRange: [0, 0.26], name: "Chroma", }, hz: { refRange: [0, 360], type: "angle", name: "Hue", }, }, base: Jzazbz, fromBase: LCHSpace.fromBase, toBase: LCHSpace.toBase, formats: { // https://drafts.csswg.org/css-color-hdr/#JzCzhz jzczhz: { coords: [" | ", " | ", " | "], }, }, }); /** * More accurate color-difference formulae * than the simple 1976 Euclidean distance in Lab * * Uses JzCzHz, which has improved perceptual uniformity * and thus a simple Euclidean root-sum of ΔL² ΔC² ΔH² * gives good results. * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @returns {number} */ function deltaEJz (color, sample) { [color, sample] = getColor([color, sample]); // Given this color as the reference // and a sample, // calculate deltaE in JzCzHz. let [Jz1, Cz1, Hz1] = jzczhz.from(color); let [Jz2, Cz2, Hz2] = jzczhz.from(sample); // Lightness and Chroma differences // sign does not matter as they are squared. let ΔJ = Jz1 - Jz2; let ΔC = Cz1 - Cz2; // length of chord for ΔH if (isNone(Hz1) && isNone(Hz2)) { // both undefined hues Hz1 = 0; Hz2 = 0; } else if (isNone(Hz1)) { // one undefined, set to the defined hue Hz1 = Hz2; } else if (isNone(Hz2)) { Hz2 = Hz1; } let Δh = Hz1 - Hz2; let ΔH = 2 * Math.sqrt(Cz1 * Cz2) * Math.sin((Δh / 2) * (Math.PI / 180)); return Math.sqrt(ΔJ ** 2 + ΔC ** 2 + ΔH ** 2); } /** @import { Matrix3x3, Vector3 } from "../types.js" */ const c1 = 3424 / 4096; const c2 = 2413 / 128; const c3 = 2392 / 128; const m1$1 = 2610 / 16384; const m2 = 2523 / 32; const im1 = 16384 / 2610; const im2 = 32 / 2523; // The matrix below includes the 4% crosstalk components // and is from the Dolby "What is ICtCp" paper" /** @type {Matrix3x3} */ // prettier-ignore const XYZtoLMS_M = [ [ 0.3592832590121217, 0.6976051147779502, -0.035891593232029 ], [ -0.1920808463704993, 1.1004767970374321, 0.0753748658519118 ], [ 0.0070797844607479, 0.0748396662186362, 0.8433265453898765 ], ]; // linear-light Rec.2020 to LMS, again with crosstalk // rational terms from Jan Fröhlich, // Encoding High Dynamic Range andWide Color Gamut Imagery, p.97 // and ITU-R BT.2124-0 p.2 /* const Rec2020toLMS_M = [ [ 1688 / 4096, 2146 / 4096, 262 / 4096 ], [ 683 / 4096, 2951 / 4096, 462 / 4096 ], [ 99 / 4096, 309 / 4096, 3688 / 4096 ] ]; */ // this includes the Ebner LMS coefficients, // the rotation, and the scaling to [-0.5,0.5] range // rational terms from Fröhlich p.97 // and ITU-R BT.2124-0 pp.2-3 /** @type {Matrix3x3} */ // prettier-ignore const LMStoIPT_M = [ [ 2048 / 4096, 2048 / 4096, 0 ], [ 6610 / 4096, -13613 / 4096, 7003 / 4096 ], [ 17933 / 4096, -17390 / 4096, -543 / 4096 ], ]; // inverted matrices, calculated from the above /** @type {Matrix3x3} */ // prettier-ignore const IPTtoLMS_M = [ [ 0.9999999999999998, 0.0086090370379328, 0.1110296250030260 ], [ 0.9999999999999998, -0.0086090370379328, -0.1110296250030259 ], [ 0.9999999999999998, 0.5600313357106791, -0.3206271749873188 ], ]; /* // prettier-ignore const LMStoRec2020_M = [ [ 3.4375568932814012112, -2.5072112125095058195, 0.069654319228104608382], [-0.79142868665644156125, 1.9838372198740089874, -0.19240853321756742626 ], [-0.025646662911506476363, -0.099240248643945566751, 1.1248869115554520431 ] ]; */ /** @type {Matrix3x3} */ // prettier-ignore const LMStoXYZ_M = [ [ 2.0701522183894223, -1.3263473389671563, 0.2066510476294053 ], [ 0.3647385209748072, 0.6805660249472273, -0.0453045459220347 ], [ -0.0497472075358123, -0.0492609666966131, 1.1880659249923042 ], ]; // Only the PQ form of ICtCp is implemented here. There is also an HLG form. // from Dolby, "WHAT IS ICTCP?" // https://professional.dolby.com/siteassets/pdfs/ictcp_dolbywhitepaper_v071.pdf // and // Dolby, "Perceptual Color Volume // Measuring the Distinguishable Colors of HDR and WCG Displays" // https://professional.dolby.com/siteassets/pdfs/dolby-vision-measuring-perceptual-color-volume-v7.1.pdf var ictcp = new ColorSpace({ id: "ictcp", name: "ICTCP", // From BT.2100-2 page 7: // During production, signal values are expected to exceed the // range E′ = [0.0 : 1.0]. This provides processing headroom and avoids // signal degradation during cascaded processing. Such values of E′, // below 0.0 or exceeding 1.0, should not be clipped during production // and exchange. // Values below 0.0 should not be clipped in reference displays (even // though they represent “negative” light) to allow the black level of // the signal (LB) to be properly set using test signals known as “PLUGE” coords: { i: { refRange: [0, 1], // Constant luminance, name: "I", }, ct: { refRange: [-0.5, 0.5], // Full BT.2020 gamut in range [-0.5, 0.5] name: "CT", }, cp: { refRange: [-0.5, 0.5], name: "CP", }, }, base: XYZ_Abs_D65, fromBase (XYZ) { // move to LMS cone domain let LMS = multiply_v3_m3x3(XYZ, XYZtoLMS_M); return LMStoICtCp(LMS); }, toBase (ICtCp) { let LMS = ICtCptoLMS(ICtCp); return multiply_v3_m3x3(LMS, LMStoXYZ_M); }, formats: { ictcp: { coords: [ " | ", " | ", " | ", ], }, }, }); /** * * @param {Vector3} LMS * @returns {Vector3} */ function LMStoICtCp (LMS) { // apply the PQ EOTF // we can't ever be dividing by zero because of the "1 +" in the denominator let PQLMS = /** @type {Vector3} */ ( LMS.map(function (val) { let num = c1 + c2 * (val / 10000) ** m1$1; let denom = 1 + c3 * (val / 10000) ** m1$1; return (num / denom) ** m2; }) ); // LMS to IPT, with rotation for Y'C'bC'r compatibility return multiply_v3_m3x3(PQLMS, LMStoIPT_M); } /** * * @param {Vector3} ICtCp * @returns {Vector3} */ function ICtCptoLMS (ICtCp) { let PQLMS = multiply_v3_m3x3(ICtCp, IPTtoLMS_M); // From BT.2124-0 Annex 2 Conversion 3 let LMS = /** @type {Vector3} */ ( PQLMS.map(function (val) { let num = Math.max(val ** im2 - c1, 0); let denom = c2 - c3 * val ** im2; return 10000 * (num / denom) ** im1; }) ); return LMS; } /** * Delta E in ICtCp space, * which the ITU calls Delta E ITP, which is shorter. * Formulae from ITU Rec. ITU-R BT.2124-0 * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @returns {number} */ function deltaEITP (color, sample) { [color, sample] = getColor([color, sample]); // Given this color as the reference // and a sample, // calculate deltaE in ICtCp // which is simply the Euclidean distance let [I1, T1, P1] = ictcp.from(color); let [I2, T2, P2] = ictcp.from(sample); // the 0.25 factor is to undo the encoding scaling in Ct // the 720 is so that 1 deltaE = 1 JND // per ITU-R BT.2124-0 p.3 return 720 * Math.sqrt((I1 - I2) ** 2 + 0.25 * (T1 - T2) ** 2 + (P1 - P2) ** 2); } /** * More accurate color-difference formulae * than the simple 1976 Euclidean distance in CIE Lab * The Oklab a and b axes are scaled relative to the L axis, for better uniformity * Björn Ottosson said: * "I've recently done some tests with color distance datasets as implemented * in Colorio and on both the Combvd dataset and the OSA-UCS dataset a * scale factor of slightly more than 2 for a and b would give the best results * (2.016 works best for Combvd and 2.045 for the OSA-UCS dataset)." * @see {@link } * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @returns {number} */ function deltaEOK2 (color, sample) { [color, sample] = getColor([color, sample]); // Given this color as the reference // and a sample, // calculate deltaEOK2, term by term as root sum of squares let abscale = 2; let [L1, a1, b1] = OKLab.from(color); let [L2, a2, b2] = OKLab.from(sample); let ΔL = L1 - L2; let Δa = abscale * (a1 - a2); let Δb = abscale * (b1 - b2); return Math.sqrt(ΔL ** 2 + Δa ** 2 + Δb ** 2); } /** @import { Coords, Matrix3x3, Vector3 } from "../types.js" */ // Type re-exports /** @typedef {import("../types.js").Cam16Object} Cam16Object */ /** @typedef {import("../types.js").Cam16Input} Cam16Input */ /** @typedef {import("../types.js").Cam16Environment} Cam16Environment */ const white$1 = WHITES.D65; const adaptedCoef = 0.42; const adaptedCoefInv = 1 / adaptedCoef; const tau = 2 * Math.PI; /** @type {Matrix3x3} */ // prettier-ignore const cat16 = [ [ 0.401288, 0.650173, -0.051461 ], [ -0.250268, 1.204414, 0.045854 ], [ -2079e-6, 0.048952, 0.953127 ], ]; /** @type {Matrix3x3} */ const cat16Inv = [ [1.8620678550872327, -1.0112546305316843, 0.14918677544445175], [0.38752654323613717, 0.6214474419314753, -0.008973985167612518], [-0.015841498849333856, -0.03412293802851557, 1.0499644368778496], ]; /** @type {Matrix3x3} */ const m1 = [ [460.0, 451.0, 288.0], [460.0, -891, -261], [460.0, -220, -6300], ]; const surroundMap = { dark: [0.8, 0.525, 0.8], dim: [0.9, 0.59, 0.9], average: [1, 0.69, 1], }; const hueQuadMap = { // Red, Yellow, Green, Blue, Red h: [20.14, 90.0, 164.25, 237.53, 380.14], e: [0.8, 0.7, 1.0, 1.2, 0.8], H: [0.0, 100.0, 200.0, 300.0, 400.0], }; const rad2deg = 180 / Math.PI; const deg2rad$1 = Math.PI / 180; /** * @param {Coords} coords * @param {number} fl * @returns {[number, number, number]} */ function adapt (coords, fl) { const temp = /** @type {[number, number, number]} */ ( coords.map(c => { const x = spow(fl * Math.abs(c) * 0.01, adaptedCoef); return (400 * copySign(x, c)) / (x + 27.13); }) ); return temp; } /** * @param {Coords} adapted * @param {number} fl * @returns {[number, number, number]} */ function unadapt (adapted, fl) { const constant = (100 / fl) * 27.13 ** adaptedCoefInv; return /** @type {[number, number, number]} */ ( adapted.map(c => { const cabs = Math.abs(c); return copySign(constant * spow(cabs / (400 - cabs), adaptedCoefInv), c); }) ); } /** * @param {number} h */ function hueQuadrature (h) { let hp = constrain$1(h); if (hp <= hueQuadMap.h[0]) { hp += 360; } const i = bisectLeft(hueQuadMap.h, hp) - 1; const [hi, hii] = hueQuadMap.h.slice(i, i + 2); const [ei, eii] = hueQuadMap.e.slice(i, i + 2); const Hi = hueQuadMap.H[i]; const t = (hp - hi) / ei; return Hi + (100 * t) / (t + (hii - hp) / eii); } /** * @param {number} H */ function invHueQuadrature (H) { let Hp = ((H % 400) + 400) % 400; const i = Math.floor(0.01 * Hp); Hp = Hp % 100; const [hi, hii] = hueQuadMap.h.slice(i, i + 2); const [ei, eii] = hueQuadMap.e.slice(i, i + 2); return constrain$1((Hp * (eii * hi - ei * hii) - 100 * hi * eii) / (Hp * (eii - ei) - 100 * eii)); } /** * @param {[number, number, number]} refWhite * @param {number} adaptingLuminance * @param {number} backgroundLuminance * @param {keyof typeof surroundMap} surround * @param {boolean} discounting * @returns {Cam16Environment} */ function environment ( refWhite, adaptingLuminance, backgroundLuminance, surround, discounting, ) { const env = {}; env.discounting = discounting; env.refWhite = refWhite; env.surround = surround; const xyzW = /** @type {Vector3} */ ( refWhite.map(c => { return c * 100; }) ); // The average luminance of the environment in `cd/m^2cd/m` (a.k.a. nits) env.la = adaptingLuminance; // The relative luminance of the nearby background env.yb = backgroundLuminance; // Absolute luminance of the reference white. const yw = xyzW[1]; // Cone response for reference white const rgbW = multiply_v3_m3x3(xyzW, cat16); // Surround: dark, dim, and average let values = surroundMap[env.surround]; const f = values[0]; env.c = values[1]; env.nc = values[2]; const k = 1 / (5 * env.la + 1); const k4 = k ** 4; // Factor of luminance level adaptation env.fl = k4 * env.la + 0.1 * (1 - k4) * (1 - k4) * Math.cbrt(5 * env.la); env.flRoot = env.fl ** 0.25; env.n = env.yb / yw; env.z = 1.48 + Math.sqrt(env.n); env.nbb = 0.725 * env.n ** -0.2; env.ncb = env.nbb; // Degree of adaptation calculating if not discounting // illuminant (assumed eye is fully adapted) const d = Math.max(Math.min(f * (1 - (1 / 3.6) * Math.exp((-env.la - 42) / 92)), 1), 0); env.dRgb = /** @type {[number, number, number]} */ ( rgbW.map(c => { return interpolate(1, yw / c, d); }) ); env.dRgbInv = /** @type {[number, number, number]} */ ( env.dRgb.map(c => { return 1 / c; }) ); // Achromatic response const rgbCW = /** @type {[number, number, number]} */ ( rgbW.map((c, i) => { return c * env.dRgb[i]; }) ); const rgbAW = adapt(rgbCW, env.fl); env.aW = env.nbb * (2 * rgbAW[0] + rgbAW[1] + 0.05 * rgbAW[2]); // console.log(env); return env; } // Pre-calculate everything we can with the viewing conditions const viewingConditions$1 = environment(white$1, (64 / Math.PI) * 0.2, 20, "average", false); /** * @param {Cam16Input} cam16 * @param {Cam16Environment} env * @returns {[number, number, number]} */ function fromCam16 (cam16, env) { // These check ensure one, and only one attribute for a // given category is provided. // @ts-expect-error The '^` operator is not allowed for boolean types if (!((cam16.J !== undefined) ^ (cam16.Q !== undefined))) { throw new Error("Conversion requires one and only one: 'J' or 'Q'"); } // @ts-expect-error - The '^` operator is not allowed for boolean types if (!((cam16.C !== undefined) ^ (cam16.M !== undefined) ^ (cam16.s !== undefined))) { throw new Error("Conversion requires one and only one: 'C', 'M' or 's'"); } // Hue is absolutely required // @ts-expect-error - The '^` operator is not allowed for boolean types if (!((cam16.h !== undefined) ^ (cam16.H !== undefined))) { throw new Error("Conversion requires one and only one: 'h' or 'H'"); } // Black if (cam16.J === 0.0 || cam16.Q === 0.0) { return [0.0, 0.0, 0.0]; } // Break hue into Cartesian components let hRad = 0.0; if (cam16.h !== undefined) { hRad = constrain$1(cam16.h) * deg2rad$1; } else { hRad = invHueQuadrature(cam16.H) * deg2rad$1; } const cosh = Math.cos(hRad); const sinh = Math.sin(hRad); // Calculate `Jroot` from one of the lightness derived coordinates. let Jroot = 0.0; if (cam16.J !== undefined) { Jroot = spow(cam16.J, 1 / 2) * 0.1; } else if (cam16.Q !== undefined) { Jroot = (0.25 * env.c * cam16.Q) / ((env.aW + 4) * env.flRoot); } // Calculate the `t` value from one of the chroma derived coordinates let alpha = 0.0; if (cam16.C !== undefined) { alpha = cam16.C / Jroot; } else if (cam16.M !== undefined) { alpha = cam16.M / env.flRoot / Jroot; } else if (cam16.s !== undefined) { alpha = (0.0004 * cam16.s ** 2 * (env.aW + 4)) / env.c; } const t = spow(alpha * Math.pow(1.64 - Math.pow(0.29, env.n), -0.73), 10 / 9); // Eccentricity const et = 0.25 * (Math.cos(hRad + 2) + 3.8); // Achromatic response const A = env.aW * spow(Jroot, 2 / env.c / env.z); // Calculate red-green and yellow-blue components const p1 = (5e4 / 13) * env.nc * env.ncb * et; const p2 = A / env.nbb; const r = 23 * (p2 + 0.305) * zdiv(t, 23 * p1 + t * (11 * cosh + 108 * sinh)); const a = r * cosh; const b = r * sinh; // Calculate back from cone response to XYZ const rgb_c = unadapt( /** @type {Vector3} */ ( multiply_v3_m3x3([p2, a, b], m1).map(c => { return (c * 1) / 1403; }) ), env.fl, ); return /** @type {Vector3} */ ( multiply_v3_m3x3( /** @type {Vector3} */ ( rgb_c.map((c, i) => { return c * env.dRgbInv[i]; }) ), cat16Inv, ).map(c => { return c / 100; }) ); } /** * @param {[number, number, number]} xyzd65 * @param {Cam16Environment} env * @returns {Cam16Object} */ function toCam16 (xyzd65, env) { // Cone response const xyz100 = /** @type {Vector3} */ ( xyzd65.map(c => { return c * 100; }) ); const rgbA = adapt( /** @type {[number, number, number]} */ ( multiply_v3_m3x3(xyz100, cat16).map((c, i) => { return c * env.dRgb[i]; }) ), env.fl, ); // Calculate hue from red-green and yellow-blue components const a = rgbA[0] + (-12 * rgbA[1] + rgbA[2]) / 11; const b = (rgbA[0] + rgbA[1] - 2 * rgbA[2]) / 9; const hRad = ((Math.atan2(b, a) % tau) + tau) % tau; // Eccentricity const et = 0.25 * (Math.cos(hRad + 2) + 3.8); const t = (5e4 / 13) * env.nc * env.ncb * zdiv(et * Math.sqrt(a ** 2 + b ** 2), rgbA[0] + rgbA[1] + 1.05 * rgbA[2] + 0.305); const alpha = spow(t, 0.9) * Math.pow(1.64 - Math.pow(0.29, env.n), 0.73); // Achromatic response const A = env.nbb * (2 * rgbA[0] + rgbA[1] + 0.05 * rgbA[2]); const Jroot = spow(A / env.aW, 0.5 * env.c * env.z); // Lightness const J = 100 * spow(Jroot, 2); // Brightness const Q = (4 / env.c) * Jroot * (env.aW + 4) * env.flRoot; // Chroma const C = alpha * Jroot; // Colorfulness const M = C * env.flRoot; // Hue const h = constrain$1(hRad * rad2deg); // Hue quadrature const H = hueQuadrature(h); // Saturation const s = 50 * spow((env.c * alpha) / (env.aW + 4), 1 / 2); // console.log({J: J, C: C, h: h, s: s, Q: Q, M: M, H: H}); return { J: J, C: C, h: h, s: s, Q: Q, M: M, H: H }; } // Provided as a way to directly evaluate the CAM16 model // https://observablehq.com/@jrus/cam16: reference implementation // https://arxiv.org/pdf/1802.06067.pdf: Nico Schlömer // https://onlinelibrary.wiley.com/doi/pdf/10.1002/col.22324: hue quadrature // https://www.researchgate.net/publication/318152296_Comprehensive_color_solutions_CAM16_CAT16_and_CAM16-UCS // Results compared against: https://github.com/colour-science/colour new ColorSpace({ id: "cam16-jmh", cssId: "--cam16-jmh", name: "CAM16-JMh", coords: { j: { refRange: [0, 100], name: "J", }, m: { refRange: [0, 105.0], name: "Colorfulness", }, h: { refRange: [0, 360], type: "angle", name: "Hue", }, }, base: xyz_d65, fromBase (xyz) { // If another derivation is created, ε could vary, so we can't hardcode if (this.ε === undefined) { this.ε = Object.values(this.coords)[1].refRange[1] / 100000; } const cam16 = toCam16(xyz, viewingConditions$1); const isAchromatic = Math.abs(cam16.M) < this.ε; return [cam16.J, isAchromatic ? 0 : cam16.M, isAchromatic ? null : cam16.h]; }, toBase (cam16) { return fromCam16({ J: cam16[0], M: cam16[1], h: cam16[2] }, viewingConditions$1); }, }); const white = WHITES.D65; const ε = 216 / 24389; // 6^3/29^3 == (24/116)^3 const κ = 24389 / 27; // 29^3/3^3 function toLstar (y) { // Convert XYZ Y to L* const fy = y > ε ? Math.cbrt(y) : (κ * y + 16) / 116; return 116.0 * fy - 16.0; } function fromLstar (lstar) { // Convert L* back to XYZ Y return lstar > 8 ? Math.pow((lstar + 16) / 116, 3) : lstar / κ; } function fromHct (coords, env) { // Use Newton's method to try and converge as quick as possible or // converge as close as we can. While the requested precision is achieved // most of the time, it may not always be achievable. Especially past the // visible spectrum, the algorithm will likely struggle to get the same // precision. If, for whatever reason, we cannot achieve the accuracy we // seek in the allotted iterations, just return the closest we were able to // get. let [h, c, t] = coords; let xyz = []; let j = 0; // Shortcut out for black if (t === 0) { return [0.0, 0.0, 0.0]; } // Calculate the Y we need to target let y = fromLstar(t); // A better initial guess yields better results. Polynomials come from // curve fitting the T vs J response. if (t > 0) { j = 0.00379058511492914 * t ** 2 + 0.608983189401032 * t + 0.9155088574762233; } else { j = 9.514440756550361e-6 * t ** 2 + 0.08693057439788597 * t - 21.928975842194614; } // Threshold of how close is close enough, and max number of attempts. // More precision and more attempts means more time spent iterating. Higher // required precision gives more accuracy but also increases the chance of // not hitting the goal. 2e-12 allows us to convert round trip with // reasonable accuracy of six decimal places or more. const threshold = 2e-12; const max_attempts = 15; let attempt = 0; let last = Infinity; // Try to find a J such that the returned y matches the returned y of the L* while (attempt <= max_attempts) { xyz = fromCam16({ J: j, C: c, h: h }, env); // If we are within range, return XYZ // If we are closer than last time, save the values const delta = Math.abs(xyz[1] - y); if (delta < last) { if (delta <= threshold) { return xyz; } last = delta; } // f(j_root) = (j ** (1 / 2)) * 0.1 // f(j) = ((f(j_root) * 100) ** 2) / j - 1 = 0 // f(j_root) = Y = y / 100 // f(j) = (y ** 2) / j - 1 // f'(j) = (2 * y) / j j = j - ((xyz[1] - y) * j) / (2 * xyz[1]); attempt += 1; } // We could not acquire the precision we desired, // return our closest attempt. return fromCam16({ J: j, C: c, h: h }, env); } function toHct (xyz, env) { // Calculate HCT by taking the L* of CIE LCh D65 and CAM16 chroma and hue. const t = toLstar(xyz[1]); if (t === 0.0) { return [0.0, 0.0, 0.0]; } const cam16 = toCam16(xyz, viewingConditions); return [constrain$1(cam16.h), cam16.C, t]; } // Pre-calculate everything we can with the viewing conditions const viewingConditions = environment( white, (200 / Math.PI) * fromLstar(50.0), fromLstar(50.0) * 100, "average", false, ); // https://material.io/blog/science-of-color-design // This is not a port of the material-color-utilities, // but instead implements the full color space as described, // combining CAM16 JCh and Lab D65. This does not clamp conversion // to HCT to specific chroma bands and provides support for wider // gamuts than Google currently supports and does so at a greater // precision (> 8 bits back to sRGB). // This implementation comes from https://github.com/facelessuser/coloraide // which is licensed under MIT. var hct = new ColorSpace({ id: "hct", name: "HCT", coords: { h: { refRange: [0, 360], type: "angle", name: "Hue", }, c: { refRange: [0, 145], name: "Colorfulness", }, t: { refRange: [0, 100], name: "Tone", }, }, base: xyz_d65, fromBase (xyz) { if (this.ε === undefined) { this.ε = Object.values(this.coords)[1].refRange[1] / 100000; } let hct = toHct(xyz); if (hct[1] < this.ε) { hct[1] = 0.0; hct[0] = null; } return hct; }, toBase (hct) { return fromHct(hct, viewingConditions); }, formats: { color: { id: "--hct", coords: [" | ", " | ", " | "], }, }, }); const deg2rad = Math.PI / 180; const ucsCoeff = [1.0, 0.007, 0.0228]; /** * Convert HCT chroma and hue (CAM16 JMh colorfulness and hue) using UCS logic for a and b. * @param {Coords} coords - HCT coordinates. * @return {number[]} */ function convertUcsAb (coords) { // We want the distance between the actual color. // If chroma is negative, it will throw off our calculations. // Normally, converting back to the base and forward will correct it. // If we have a negative chroma after this, then we have a color that // cannot resolve to positive chroma. if (coords[1] < 0) { coords = hct.fromBase(hct.toBase(coords)); } // Only in extreme cases (usually outside the visible spectrum) // can the input value for log become negative. // Avoid domain error by forcing a zero result via "max" if necessary. const M = Math.log(Math.max(1 + ucsCoeff[2] * coords[1] * viewingConditions.flRoot, 1.0)) / ucsCoeff[2]; const hrad = coords[0] * deg2rad; const a = M * Math.cos(hrad); const b = M * Math.sin(hrad); return [coords[2], a, b]; } /** * Color distance using HCT. * @param {import("../types.js").ColorTypes} color * @param {import("../types.js").ColorTypes} sample * @returns {number} */ function deltaEHCT (color, sample) { [color, sample] = getColor([color, sample]); let [t1, a1, b1] = convertUcsAb(hct.from(color)); let [t2, a2, b2] = convertUcsAb(hct.from(sample)); // Use simple euclidean distance with a and b using UCS conversion // and LCh lightness (HCT tone). return Math.sqrt((t1 - t2) ** 2 + (a1 - a2) ** 2 + (b1 - b2) ** 2); } /** * @packageDocumentation * This module defines all the builtin deltaE methods. */ var deltaEMethods = { deltaE76, deltaECMC, deltaE2000, deltaEJz, deltaEITP, deltaEOK, deltaEOK2, deltaEHCT, }; /** @typedef {keyof typeof import("./index.js").default extends `deltaE${infer Method}` ? Method : string} Methods */ /** @import { ColorTypes, PlainColorObject } from "./types.js" */ // Type re-exports /** @typedef {import("./types.js").ToGamutOptions} ToGamutOptions */ /** * Calculate the epsilon to 2 degrees smaller than the specified JND. * @param {number} jnd The target "just noticeable difference". * @returns {number} */ function calcEpsilon (jnd) { // Calculate the epsilon to 2 degrees smaller than the specified JND. const order = !jnd ? 0 : Math.floor(Math.log10(Math.abs(jnd))); // Limit to an arbitrary value to ensure value is never too small and causes infinite loops. return Math.max(parseFloat(`1e${order - 2}`), 1e-6); } const GMAPPRESET = { hct: { method: "hct.c", jnd: 2, deltaEMethod: "hct", blackWhiteClamp: {}, }, "hct-tonal": { method: "hct.c", jnd: 0, deltaEMethod: "hct", blackWhiteClamp: { channel: "hct.t", min: 0, max: 100 }, }, }; /** * Force coordinates to be in gamut of a certain color space. * Mutates the color it is passed. * @overload * @param {ColorTypes} color * @param {ToGamutOptions} [options] * @returns {PlainColorObject} */ /** * @overload * @param {ColorTypes} color * @param {string} [space] * @returns {PlainColorObject} */ /** * @param {ColorTypes} color * @param {string & Partial | ToGamutOptions} [space] * @returns {PlainColorObject} */ function toGamut ( color, { method = defaults$1.gamut_mapping, space = undefined, deltaEMethod = "", jnd = 2, blackWhiteClamp = undefined, } = {}, ) { color = getColor(color); if (isString(arguments[1])) { space = arguments[1]; } else if (!space) { space = color.space; } space = ColorSpace.get(space); // 3 spaces: // color.space: current color space // space: space whose gamut we are mapping to // mapSpace: space with the coord we're reducing if (inGamut(color, space, { epsilon: 0 })) { return /** @type {PlainColorObject} */ (color); } let spaceColor; if (method === "css") { spaceColor = toGamutCSS(color, { space }); } else { if (method !== "clip" && !inGamut(color, space)) { if (Object.prototype.hasOwnProperty.call(GMAPPRESET, method)) { ({ method, jnd, deltaEMethod, blackWhiteClamp } = GMAPPRESET[method]); } // Get the correct delta E method let de = deltaE2000; if (deltaEMethod !== "") { for (let m in deltaEMethods) { if ("deltae" + deltaEMethod.toLowerCase() === m.toLowerCase()) { de = deltaEMethods[m]; break; } } } if (jnd === 0) { jnd = 1e-16; } let clipped = toGamut(to(color, space), { method: "clip", space }); if (de(color, clipped) > jnd) { // Clamp to SDR white and black if required if (blackWhiteClamp && Object.keys(blackWhiteClamp).length === 3) { let channelMeta = ColorSpace.resolveCoord(blackWhiteClamp.channel); let channel = get$1(to(color, channelMeta.space), channelMeta.id); if (isNone(channel)) { channel = 0; } if (channel >= blackWhiteClamp.max) { return to({ space: "xyz-d65", coords: WHITES["D65"] }, color.space); } else if (channel <= blackWhiteClamp.min) { return to({ space: "xyz-d65", coords: [0, 0, 0] }, color.space); } } // Reduce a coordinate of a certain color space until the color is in gamut let coordMeta = ColorSpace.resolveCoord(method); let mapSpace = coordMeta.space; let coordId = coordMeta.id; let mappedColor = to(color, mapSpace); // If we were already in the mapped color space, we need to resolve undefined channels mappedColor.coords.forEach((c, i) => { if (isNone(c)) { mappedColor.coords[i] = 0; } }); let bounds = coordMeta.range || coordMeta.refRange; let min = bounds[0]; let ε = calcEpsilon(jnd); let low = min; let high = get$1(mappedColor, coordId); while (high - low > ε) { let clipped = clone$1(mappedColor); clipped = toGamut(clipped, { space, method: "clip" }); let deltaE = de(mappedColor, clipped); if (deltaE - jnd < ε) { low = get$1(mappedColor, coordId); } else { high = get$1(mappedColor, coordId); } set(mappedColor, coordId, (low + high) / 2); } spaceColor = to(mappedColor, space); } else { spaceColor = clipped; } } else { spaceColor = to(color, space); } if ( method === "clip" || // Dumb coord clipping // finish off smarter gamut mapping with clip to get rid of ε, see #17 !inGamut(spaceColor, space, { epsilon: 0 }) ) { let bounds = Object.values(space.coords).map(c => c.range || []); spaceColor.coords = /** @type {[number, number, number]} */ ( spaceColor.coords.map((c, i) => { let [min, max] = bounds[i]; if (min !== undefined) { c = Math.max(min, c); } if (max !== undefined) { c = Math.min(c, max); } return c; }) ); } } if (space !== color.space) { spaceColor = to(spaceColor, color.space); } color.coords = spaceColor.coords; return /** @type {PlainColorObject} */ (color); } /** @type {"color"} */ toGamut.returns = "color"; /** * The reference colors to be used if lightness is out of the range 0-1 in the * `Oklch` space. These are created in the `Oklab` space, as it is used by the * DeltaEOK calculation, so it is guaranteed to be imported. * @satisfies {Record} */ const COLORS = { WHITE: { space: OKLab, coords: [1, 0, 0], alpha: 1 }, BLACK: { space: OKLab, coords: [0, 0, 0], alpha: 1 }, }; /** * Given a color `origin`, returns a new color that is in gamut using * the CSS Gamut Mapping Algorithm. If `space` is specified, it will be in gamut * in `space`, and returned in `space`. Otherwise, it will be in gamut and * returned in the color space of `origin`. * @param {ColorTypes} origin * @param {{ space?: string | ColorSpace | undefined }} param1 * @returns {PlainColorObject} */ function toGamutCSS (origin, { space } = {}) { const JND = 0.02; const ε = 0.0001; origin = getColor(origin); if (!space) { space = origin.space; } space = ColorSpace.get(space); const oklchSpace = ColorSpace.get("oklch"); if (space.isUnbounded) { return to(origin, space); } const origin_OKLCH = to(origin, oklchSpace); let L = origin_OKLCH.coords[0]; // return media white or black, if lightness is out of range if (L >= 1) { const white = to(COLORS.WHITE, space); white.alpha = origin.alpha; return to(white, space); } if (L <= 0) { const black = to(COLORS.BLACK, space); black.alpha = origin.alpha; return to(black, space); } if (inGamut(origin_OKLCH, space, { epsilon: 0 })) { return to(origin_OKLCH, space); } function clip (_color) { const destColor = to(_color, space); const spaceCoords = Object.values(/** @type {ColorSpace} */ (space).coords); destColor.coords = /** @type {[number, number, number]} */ ( destColor.coords.map((coord, index) => { if ("range" in spaceCoords[index]) { const [min, max] = spaceCoords[index].range; return clamp(min, coord, max); } return coord; }) ); return destColor; } let min = 0; let max = origin_OKLCH.coords[1]; let min_inGamut = true; let current = clone$1(origin_OKLCH); let clipped = clip(current); let E = deltaEOK(clipped, current); if (E < JND) { return clipped; } while (max - min > ε) { const chroma = (min + max) / 2; current.coords[1] = chroma; if (min_inGamut && inGamut(current, space, { epsilon: 0 })) { min = chroma; } else { clipped = clip(current); E = deltaEOK(clipped, current); if (E < JND) { if (JND - E < ε) { break; } else { min_inGamut = false; min = chroma; } } else { max = chroma; } } } return clipped; } /** @import { ColorTypes, PlainColorObject, ToGamutOptions } from "./types.js" */ /** * Convert to color space and return a new color * @param {ColorTypes} color * @param {string | ColorSpace} space * @param {{ inGamut?: boolean | ToGamutOptions | undefined }} options * @returns {PlainColorObject} */ function to (color, space, { inGamut } = {}) { color = getColor(color); space = ColorSpace.get(space); let coords = space.from(color); let ret = { space, coords, alpha: color.alpha }; if (inGamut) { ret = toGamut(ret, inGamut === true ? undefined : inGamut); } return ret; } /** @type {"color"} */ to.returns = "color"; // @ts-nocheck /** @import { ColorTypes, ParseOptions, PlainColorObject } from "./types.js" */ // Type re-exports /** @typedef {import("./types.js").SerializeOptions} SerializeOptions */ /** * Generic toString() method, outputs a color(spaceId ...coords) function, a functional syntax, or custom formats defined by the color space * @param {ColorTypes} color * @param {SerializeOptions & Record} options * @returns {string} */ function serialize (color, options = {}) { let { precision = defaults$1.precision, format, inGamut: inGamut$1 = true, coords: coordFormat, alpha: alphaFormat, commas, } = options; let ret; let colorWithMeta = /** @type {PlainColorObject & ParseOptions} */ (getColor(color)); let formatId = format; let parseMeta = colorWithMeta.parseMeta; if (parseMeta && !format) { if (parseMeta.format.canSerialize()) { format = parseMeta.format; formatId = parseMeta.formatId; } coordFormat ??= parseMeta.types; alphaFormat ??= parseMeta.alphaType; commas ??= parseMeta.commas; } if (formatId) { // A format is explicitly specified format = colorWithMeta.space.getFormat(format) ?? ColorSpace.findFormat(formatId); } if (!format) { // No format specified, or format not found format = colorWithMeta.space.getFormat("default") ?? ColorSpace.DEFAULT_FORMAT; formatId = format.name; } if (format && format.space && format.space !== colorWithMeta.space) { // Format specified belongs to a different color space, // need to convert to it first colorWithMeta = to(colorWithMeta, format.space); } // The assignment to coords and inGamut needs to stay in the order they are now // The order of the assignment was changed as a workaround for a bug in Next.js // See this issue for details: https://github.com/color-js/color.js/issues/260 let coords = colorWithMeta.coords.slice(); // clone so we can manipulate it inGamut$1 ||= format.toGamut; if (inGamut$1 && !inGamut(colorWithMeta)) { // FIXME what happens if the color contains none values? coords = toGamut(clone$1(colorWithMeta), inGamut$1 === true ? undefined : inGamut$1).coords; } if (format.type === "custom") { if (format.serialize) { ret = format.serialize(coords, colorWithMeta.alpha, options); } else { throw new TypeError( `format ${formatId} can only be used to parse colors, not for serialization`, ); } } else { // Functional syntax let name = format.name || "color"; let args = format.serializeCoords(coords, precision, coordFormat); if (name === "color") { // If output is a color() function, add colorspace id as first argument let cssId = format.id || format.ids?.[0] || colorWithMeta.space.cssId || colorWithMeta.space.id; args.unshift(cssId); } // Serialize alpha? /** @type {string | number} */ let alpha = colorWithMeta.alpha; if (alphaFormat !== undefined && !(typeof alphaFormat === "object")) { alphaFormat = typeof alphaFormat === "string" ? { type: alphaFormat } : { include: alphaFormat }; } let alphaType = alphaFormat?.type ?? ""; let serializeAlpha = alphaFormat?.include === true || format.alpha === true || (alphaFormat?.include !== false && format.alpha !== false && alpha < 1); let strAlpha = ""; commas ??= format.commas; if (serializeAlpha) { if (precision !== null) { let unit; if (alphaType === "") { unit = "%"; alpha *= 100; } alpha = serializeNumber(alpha, { precision, unit }); } strAlpha = `${commas ? "," : " /"} ${alpha}`; } ret = `${name}(${args.join(commas ? ", " : " ")}${strAlpha})`; } return ret; } /** @import { Matrix3x3 } from "../types.js" */ /** @type {Matrix3x3} */ // prettier-ignore const toXYZ_M$1 = [ [0.4865709486482162, 0.26566769316909306, 0.1982172852343625], [0.2289745640697488, 0.6917385218365064, 0.079286914093745], [0.0000000000000000, 0.04511338185890264, 1.043944368900976], ]; /** @type {Matrix3x3} */ // prettier-ignore const fromXYZ_M$1 = [ [ 2.493496911941425, -0.9313836179191239, -0.40271078445071684], [-0.8294889695615747, 1.7626640603183463, 0.023624685841943577], [ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872], ]; var P3Linear = new RGBColorSpace({ id: "p3-linear", cssId: "display-p3-linear", name: "Linear P3", white: "D65", toXYZ_M: toXYZ_M$1, fromXYZ_M: fromXYZ_M$1, }); /** @import { Matrix3x3 } from "../types.js" */ // This is the linear-light version of sRGB // as used for example in SVG filters // or in Canvas // This matrix was calculated directly from the RGB and white chromaticities // when rounded to 8 decimal places, it agrees completely with the official matrix // see https://github.com/w3c/csswg-drafts/issues/5922 /** @type {Matrix3x3} */ // prettier-ignore const toXYZ_M = [ [ 0.41239079926595934, 0.357584339383878, 0.1804807884018343 ], [ 0.21263900587151027, 0.715168678767756, 0.07219231536073371 ], [ 0.01933081871559182, 0.11919477979462598, 0.9505321522496607 ], ]; // This matrix is the inverse of the above; // again it agrees with the official definition when rounded to 8 decimal places /** @type {Matrix3x3} */ // prettier-ignore const fromXYZ_M = [ [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], ]; var sRGBLinear = new RGBColorSpace({ id: "srgb-linear", name: "Linear sRGB", white: "D65", toXYZ_M, fromXYZ_M, }); // To produce: Visit https://www.w3.org/TR/css-color-4/#named-colors // and run in the console: // copy($$("tr", $(".named-color-table tbody")).map(tr => `"${tr.cells[2].textContent.trim()}": [${tr.cells[4].textContent.trim().split(/\s+/).map(c => c === "0"? "0" : c === "255"? "1" : c + " / 255").join(", ")}]`).join(",\n")) /** List of CSS color keywords * Note that this does not include currentColor, transparent, * or system colors * * @type {Record} */ var KEYWORDS = { aliceblue: [240 / 255, 248 / 255, 1], antiquewhite: [250 / 255, 235 / 255, 215 / 255], aqua: [0, 1, 1], aquamarine: [127 / 255, 1, 212 / 255], azure: [240 / 255, 1, 1], beige: [245 / 255, 245 / 255, 220 / 255], bisque: [1, 228 / 255, 196 / 255], black: [0, 0, 0], blanchedalmond: [1, 235 / 255, 205 / 255], blue: [0, 0, 1], blueviolet: [138 / 255, 43 / 255, 226 / 255], brown: [165 / 255, 42 / 255, 42 / 255], burlywood: [222 / 255, 184 / 255, 135 / 255], cadetblue: [95 / 255, 158 / 255, 160 / 255], chartreuse: [127 / 255, 1, 0], chocolate: [210 / 255, 105 / 255, 30 / 255], coral: [1, 127 / 255, 80 / 255], cornflowerblue: [100 / 255, 149 / 255, 237 / 255], cornsilk: [1, 248 / 255, 220 / 255], crimson: [220 / 255, 20 / 255, 60 / 255], cyan: [0, 1, 1], darkblue: [0, 0, 139 / 255], darkcyan: [0, 139 / 255, 139 / 255], darkgoldenrod: [184 / 255, 134 / 255, 11 / 255], darkgray: [169 / 255, 169 / 255, 169 / 255], darkgreen: [0, 100 / 255, 0], darkgrey: [169 / 255, 169 / 255, 169 / 255], darkkhaki: [189 / 255, 183 / 255, 107 / 255], darkmagenta: [139 / 255, 0, 139 / 255], darkolivegreen: [85 / 255, 107 / 255, 47 / 255], darkorange: [1, 140 / 255, 0], darkorchid: [153 / 255, 50 / 255, 204 / 255], darkred: [139 / 255, 0, 0], darksalmon: [233 / 255, 150 / 255, 122 / 255], darkseagreen: [143 / 255, 188 / 255, 143 / 255], darkslateblue: [72 / 255, 61 / 255, 139 / 255], darkslategray: [47 / 255, 79 / 255, 79 / 255], darkslategrey: [47 / 255, 79 / 255, 79 / 255], darkturquoise: [0, 206 / 255, 209 / 255], darkviolet: [148 / 255, 0, 211 / 255], deeppink: [1, 20 / 255, 147 / 255], deepskyblue: [0, 191 / 255, 1], dimgray: [105 / 255, 105 / 255, 105 / 255], dimgrey: [105 / 255, 105 / 255, 105 / 255], dodgerblue: [30 / 255, 144 / 255, 1], firebrick: [178 / 255, 34 / 255, 34 / 255], floralwhite: [1, 250 / 255, 240 / 255], forestgreen: [34 / 255, 139 / 255, 34 / 255], fuchsia: [1, 0, 1], gainsboro: [220 / 255, 220 / 255, 220 / 255], ghostwhite: [248 / 255, 248 / 255, 1], gold: [1, 215 / 255, 0], goldenrod: [218 / 255, 165 / 255, 32 / 255], gray: [128 / 255, 128 / 255, 128 / 255], green: [0, 128 / 255, 0], greenyellow: [173 / 255, 1, 47 / 255], grey: [128 / 255, 128 / 255, 128 / 255], honeydew: [240 / 255, 1, 240 / 255], hotpink: [1, 105 / 255, 180 / 255], indianred: [205 / 255, 92 / 255, 92 / 255], indigo: [75 / 255, 0, 130 / 255], ivory: [1, 1, 240 / 255], khaki: [240 / 255, 230 / 255, 140 / 255], lavender: [230 / 255, 230 / 255, 250 / 255], lavenderblush: [1, 240 / 255, 245 / 255], lawngreen: [124 / 255, 252 / 255, 0], lemonchiffon: [1, 250 / 255, 205 / 255], lightblue: [173 / 255, 216 / 255, 230 / 255], lightcoral: [240 / 255, 128 / 255, 128 / 255], lightcyan: [224 / 255, 1, 1], lightgoldenrodyellow: [250 / 255, 250 / 255, 210 / 255], lightgray: [211 / 255, 211 / 255, 211 / 255], lightgreen: [144 / 255, 238 / 255, 144 / 255], lightgrey: [211 / 255, 211 / 255, 211 / 255], lightpink: [1, 182 / 255, 193 / 255], lightsalmon: [1, 160 / 255, 122 / 255], lightseagreen: [32 / 255, 178 / 255, 170 / 255], lightskyblue: [135 / 255, 206 / 255, 250 / 255], lightslategray: [119 / 255, 136 / 255, 153 / 255], lightslategrey: [119 / 255, 136 / 255, 153 / 255], lightsteelblue: [176 / 255, 196 / 255, 222 / 255], lightyellow: [1, 1, 224 / 255], lime: [0, 1, 0], limegreen: [50 / 255, 205 / 255, 50 / 255], linen: [250 / 255, 240 / 255, 230 / 255], magenta: [1, 0, 1], maroon: [128 / 255, 0, 0], mediumaquamarine: [102 / 255, 205 / 255, 170 / 255], mediumblue: [0, 0, 205 / 255], mediumorchid: [186 / 255, 85 / 255, 211 / 255], mediumpurple: [147 / 255, 112 / 255, 219 / 255], mediumseagreen: [60 / 255, 179 / 255, 113 / 255], mediumslateblue: [123 / 255, 104 / 255, 238 / 255], mediumspringgreen: [0, 250 / 255, 154 / 255], mediumturquoise: [72 / 255, 209 / 255, 204 / 255], mediumvioletred: [199 / 255, 21 / 255, 133 / 255], midnightblue: [25 / 255, 25 / 255, 112 / 255], mintcream: [245 / 255, 1, 250 / 255], mistyrose: [1, 228 / 255, 225 / 255], moccasin: [1, 228 / 255, 181 / 255], navajowhite: [1, 222 / 255, 173 / 255], navy: [0, 0, 128 / 255], oldlace: [253 / 255, 245 / 255, 230 / 255], olive: [128 / 255, 128 / 255, 0], olivedrab: [107 / 255, 142 / 255, 35 / 255], orange: [1, 165 / 255, 0], orangered: [1, 69 / 255, 0], orchid: [218 / 255, 112 / 255, 214 / 255], palegoldenrod: [238 / 255, 232 / 255, 170 / 255], palegreen: [152 / 255, 251 / 255, 152 / 255], paleturquoise: [175 / 255, 238 / 255, 238 / 255], palevioletred: [219 / 255, 112 / 255, 147 / 255], papayawhip: [1, 239 / 255, 213 / 255], peachpuff: [1, 218 / 255, 185 / 255], peru: [205 / 255, 133 / 255, 63 / 255], pink: [1, 192 / 255, 203 / 255], plum: [221 / 255, 160 / 255, 221 / 255], powderblue: [176 / 255, 224 / 255, 230 / 255], purple: [128 / 255, 0, 128 / 255], rebeccapurple: [102 / 255, 51 / 255, 153 / 255], red: [1, 0, 0], rosybrown: [188 / 255, 143 / 255, 143 / 255], royalblue: [65 / 255, 105 / 255, 225 / 255], saddlebrown: [139 / 255, 69 / 255, 19 / 255], salmon: [250 / 255, 128 / 255, 114 / 255], sandybrown: [244 / 255, 164 / 255, 96 / 255], seagreen: [46 / 255, 139 / 255, 87 / 255], seashell: [1, 245 / 255, 238 / 255], sienna: [160 / 255, 82 / 255, 45 / 255], silver: [192 / 255, 192 / 255, 192 / 255], skyblue: [135 / 255, 206 / 255, 235 / 255], slateblue: [106 / 255, 90 / 255, 205 / 255], slategray: [112 / 255, 128 / 255, 144 / 255], slategrey: [112 / 255, 128 / 255, 144 / 255], snow: [1, 250 / 255, 250 / 255], springgreen: [0, 1, 127 / 255], steelblue: [70 / 255, 130 / 255, 180 / 255], tan: [210 / 255, 180 / 255, 140 / 255], teal: [0, 128 / 255, 128 / 255], thistle: [216 / 255, 191 / 255, 216 / 255], tomato: [1, 99 / 255, 71 / 255], turquoise: [64 / 255, 224 / 255, 208 / 255], violet: [238 / 255, 130 / 255, 238 / 255], wheat: [245 / 255, 222 / 255, 179 / 255], white: [1, 1, 1], whitesmoke: [245 / 255, 245 / 255, 245 / 255], yellow: [1, 1, 0], yellowgreen: [154 / 255, 205 / 255, 50 / 255], }; /** @import { Coords } from "../types.js" */ let coordGrammar = Array(3).fill(" | [0, 255]"); let coordGrammarNumber = Array(3).fill("[0, 255]"); var sRGB = new RGBColorSpace({ id: "srgb", name: "sRGB", base: sRGBLinear, fromBase: rgb => { // convert an array of linear-light sRGB values in the range 0.0-1.0 // to gamma corrected form // https://en.wikipedia.org/wiki/SRGB return rgb.map(val => { let sign = val < 0 ? -1 : 1; let abs = val * sign; if (abs > 0.0031308) { return sign * (1.055 * abs ** (1 / 2.4) - 0.055); } return 12.92 * val; }); }, toBase: rgb => { // convert an array of sRGB values in the range 0.0 - 1.0 // to linear light (un-companded) form. // https://en.wikipedia.org/wiki/SRGB return rgb.map(val => { let sign = val < 0 ? -1 : 1; let abs = val * sign; if (abs <= 0.04045) { return val / 12.92; } return sign * ((abs + 0.055) / 1.055) ** 2.4; }); }, formats: { rgb: { coords: coordGrammar, }, rgb_number: { name: "rgb", commas: true, coords: coordGrammarNumber, alpha: false, }, color: { /* use defaults */ }, rgba: { coords: coordGrammar, commas: true, alpha: true, }, rgba_number: { name: "rgba", commas: true, coords: coordGrammarNumber, }, hex: { type: "custom", toGamut: true, test: str => /^#(([a-f0-9]{2}){3,4}|[a-f0-9]{3,4})$/i.test(str), parse (str) { if (str.length <= 5) { // #rgb or #rgba, duplicate digits str = str.replace(/[a-f0-9]/gi, "$&$&"); } /** @type {number[]} */ let rgba = []; // @ts-expect-error Type 'void' is not assignable to type 'string' str.replace(/[a-f0-9]{2}/gi, component => { rgba.push(parseInt(component, 16) / 255); }); return { spaceId: "srgb", coords: /** @type {Coords} */ (rgba.slice(0, 3)), alpha: /** @type {number} */ (rgba.slice(3)[0]), }; }, serialize: ( coords, alpha, { collapse = true, // collapse to 3-4 digit hex when possible? alpha: alphaFormat, } = {}, ) => { if ((alphaFormat !== false && alpha < 1) || alphaFormat === true) { coords.push(alpha); } coords = /** @type {[number, number, number]} */ ( coords.map(c => Math.round(c * 255)) ); let collapsible = collapse && coords.every(c => c % 17 === 0); let hex = coords .map(c => { if (collapsible) { return (c / 17).toString(16); } return c.toString(16).padStart(2, "0"); }) .join(""); return "#" + hex; }, }, keyword: { type: "custom", test: str => /^[a-z]+$/i.test(str), parse (str) { str = str.toLowerCase(); let ret = { spaceId: "srgb", coords: null, alpha: 1 }; if (str === "transparent") { ret.coords = KEYWORDS.black; ret.alpha = 0; } else { ret.coords = KEYWORDS[str]; } if (ret.coords) { return ret; } }, }, }, }); var P3 = new RGBColorSpace({ id: "p3", cssId: "display-p3", name: "P3", base: P3Linear, // Gamma encoding/decoding is the same as sRGB fromBase: sRGB.fromBase, toBase: sRGB.toBase, }); /** * Relative luminance */ /** @import { ColorTypes } from "./types.js" */ /** * * @param {ColorTypes} color * @returns {number} */ function getLuminance (color) { // Assume getColor() is called on color in get() return get$1(color, [xyz_d65, "y"]); } // WCAG 2.0 contrast https://www.w3.org/TR/WCAG20-TECHS/G18.html // Simple contrast, with fixed 5% viewing flare contribution // Symmetric, does not matter which is foreground and which is background /** * @param {import("../types.js").ColorTypes} color1 * @param {import("../types.js").ColorTypes} color2 * @returns {number} */ function contrastWCAG21 (color1, color2) { color1 = getColor(color1); color2 = getColor(color2); let Y1 = Math.max(getLuminance(color1), 0); let Y2 = Math.max(getLuminance(color2), 0); if (Y2 > Y1) { [Y1, Y2] = [Y2, Y1]; } return (Y1 + 0.05) / (Y2 + 0.05); } // APCA 0.0.98G // https://github.com/Myndex/apca-w3 // see also https://github.com/w3c/silver/issues/643 // exponents const normBG = 0.56; const normTXT = 0.57; const revTXT = 0.62; const revBG = 0.65; // clamps const blkThrs = 0.022; const blkClmp = 1.414; const loClip = 0.1; const deltaYmin = 0.0005; // scalers // see https://github.com/w3c/silver/issues/645 const scaleBoW = 1.14; const loBoWoffset = 0.027; const scaleWoB = 1.14; function fclamp (Y) { if (Y >= blkThrs) { return Y; } return Y + (blkThrs - Y) ** blkClmp; } function linearize (val) { let sign = val < 0 ? -1 : 1; let abs = Math.abs(val); return sign * Math.pow(abs, 2.4); } /** * Not symmetric, requires a foreground (text) color, and a background color * @param {import("../types.js").ColorTypes} background * @param {import("../types.js").ColorTypes} foreground * @returns {number} */ function contrastAPCA (background, foreground) { foreground = getColor(foreground); background = getColor(background); let S; let C; let Sapc; // Myndex as-published, assumes sRGB inputs let R, G, B; foreground = to(foreground, "srgb"); // Should these be clamped to in-gamut values? // Calculates "screen luminance" with non-standard simple gamma EOTF // weights should be from CSS Color 4, not the ones here which are via Myndex and copied from Lindbloom [R, G, B] = foreground.coords.map(c => { return isNone(c) ? 0 : c; }); let lumTxt = linearize(R) * 0.2126729 + linearize(G) * 0.7151522 + linearize(B) * 0.072175; background = to(background, "srgb"); [R, G, B] = background.coords.map(c => { return isNone(c) ? 0 : c; }); let lumBg = linearize(R) * 0.2126729 + linearize(G) * 0.7151522 + linearize(B) * 0.072175; // toe clamping of very dark values to account for flare let Ytxt = fclamp(lumTxt); let Ybg = fclamp(lumBg); // are we "Black on White" (dark on light), or light on dark? let BoW = Ybg > Ytxt; // why is this a delta, when Y is not perceptually uniform? // Answer: it is a noise gate, see // https://github.com/LeaVerou/color.js/issues/208 if (Math.abs(Ybg - Ytxt) < deltaYmin) { C = 0; } else { if (BoW) { // dark text on light background S = Ybg ** normBG - Ytxt ** normTXT; C = S * scaleBoW; } else { // light text on dark background S = Ybg ** revBG - Ytxt ** revTXT; C = S * scaleWoB; } } if (Math.abs(C) < loClip) { Sapc = 0; } else if (C > 0) { // not clear whether Woffset is loBoWoffset or loWoBoffset // but they have the same value Sapc = C - loBoWoffset; } else { Sapc = C + loBoWoffset; } return Sapc * 100; } /** * Functions related to color interpolation */ /** * Creates a function that accepts a number and returns a color. * For numbers in the range 0 to 1, the function interpolates; * for numbers outside that range, the function extrapolates * (and thus may not return the results you expect) * @overload * @param {Range} range * @param {RangeOptions} [options] * @returns {Range} */ /** * @overload * @param {ColorTypes} color1 * @param {ColorTypes} color2 * @param {RangeOptions & Record} [options] * @returns {Range} */ function range (color1, color2, options = {}) { if (isRange(color1)) { // Tweaking existing range let [r, options] = [color1, color2]; return range(...r.rangeArgs.colors, { ...r.rangeArgs.options, ...options }); } let { space, outputSpace, progression, premultiplied } = options; color1 = getColor(color1); color2 = getColor(color2); // Make sure we're working on copies of these colors color1 = clone$1(color1); color2 = clone$1(color2); let rangeArgs = { colors: [color1, color2], options }; if (space) { space = ColorSpace.get(space); } else { space = ColorSpace.registry[defaults$1.interpolationSpace] || color1.space; } outputSpace = outputSpace ? ColorSpace.get(outputSpace) : space; color1 = to(color1, space); color2 = to(color2, space); // Gamut map to avoid areas of flat color color1 = toGamut(color1); color2 = toGamut(color2); // Handle hue interpolation // See https://github.com/w3c/csswg-drafts/issues/4735#issuecomment-635741840 if (space.coords.h && space.coords.h.type === "angle") { let arc = (options.hue = options.hue || "shorter"); let /** @type {Ref} */ hue = [space, "h"]; let [θ1, θ2] = [get$1(color1, hue), get$1(color2, hue)]; // Undefined hues must be evaluated before hue fix-up to properly // calculate hue arcs between undefined and defined hues. // See https://github.com/w3c/csswg-drafts/issues/9436#issuecomment-1746957545 if (isNone(θ1) && !isNone(θ2)) { θ1 = θ2; } else if (isNone(θ2) && !isNone(θ1)) { θ2 = θ1; } [θ1, θ2] = adjust(arc, [θ1, θ2]); set(color1, hue, θ1); set(color2, hue, θ2); } if (premultiplied) { // not coping with polar spaces yet color1.coords = /** @type {[number, number, number]} */ ( color1.coords.map(c => c * color1.alpha) ); color2.coords = /** @type {[number, number, number]} */ ( color2.coords.map(c => c * color2.alpha) ); } return Object.assign( p => { p = progression ? progression(p) : p; let coords = color1.coords.map((start, i) => { let end = color2.coords[i]; return interpolate(start, end, p); }); let alpha = interpolate(color1.alpha, color2.alpha, p); let ret = { space, coords, alpha }; if (premultiplied) { // undo premultiplication ret.coords = ret.coords.map(c => c / alpha); } if (outputSpace !== space) { ret = to(ret, outputSpace); } return ret; }, { rangeArgs, }, ); } /** * @param {any} val * @returns {val is Range} */ function isRange (val) { return type$1(val) === "function" && !!val.rangeArgs; } defaults$1.interpolationSpace = "lab"; var HSLSpace = new ColorSpace({ id: "hsl", name: "HSL", coords: { h: { refRange: [0, 360], type: "angle", name: "Hue", }, s: { range: [0, 100], name: "Saturation", }, l: { range: [0, 100], name: "Lightness", }, }, base: sRGB, // Adapted from https://drafts.csswg.org/css-color-4/better-rgbToHsl.js fromBase: rgb => { let max = Math.max(...rgb); let min = Math.min(...rgb); let [r, g, b] = rgb; let [h, s, l] = [null, 0, (min + max) / 2]; let d = max - min; if (d !== 0) { s = l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; } h = h * 60; } // Very out of gamut colors can produce negative saturation // If so, just rotate the hue by 180 and use a positive saturation // see https://github.com/w3c/csswg-drafts/issues/9222 if (s < 0) { h += 180; s = Math.abs(s); } if (h >= 360) { h -= 360; } return [h, s * 100, l * 100]; }, // Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative toBase: hsl => { let [h, s, l] = hsl; h = h % 360; if (h < 0) { h += 360; } s /= 100; l /= 100; function f (n) { let k = (n + h / 30) % 12; let a = s * Math.min(l, 1 - l); return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); } return [f(0), f(8), f(4)]; }, formats: { hsl: { coords: [" | ", " | ", " | "], }, hsla: { coords: [" | ", " | ", " | "], commas: true, alpha: true, }, }, }); // Note that, like HSL, calculations are done directly on // gamma-corrected sRGB values rather than linearising them first. var HSV = new ColorSpace({ id: "hsv", name: "HSV", coords: { h: { refRange: [0, 360], type: "angle", name: "Hue", }, s: { range: [0, 100], name: "Saturation", }, v: { range: [0, 100], name: "Value", }, }, base: sRGB, // https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation fromBase (rgb) { let max = Math.max(...rgb); let min = Math.min(...rgb); let [r, g, b] = rgb; let [h, s, v] = [null, 0, max]; let d = max - min; if (d !== 0) { switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; } h = h * 60; } if (v) { s = d / v; } if (h >= 360) { h -= 360; } return [h, s * 100, v * 100]; }, // Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative toBase (hsv) { let [h, s, v] = hsv; h = h % 360; if (h < 0) { h += 360; } s /= 100; v /= 100; function f (n) { let k = (n + h / 60) % 6; return v - v * s * Math.max(0, Math.min(k, 4 - k, 1)); } return [f(5), f(3), f(1)]; }, formats: { color: { id: "--hsv", coords: [" | ", " | ", " | "], }, }, }); // The Hue, Whiteness Blackness (HWB) colorspace // See https://drafts.csswg.org/css-color-4/#the-hwb-notation // Note that, like HSL, calculations are done directly on // gamma-corrected sRGB values rather than linearising them first. var HWBSpace = new ColorSpace({ id: "hwb", name: "HWB", coords: { h: { refRange: [0, 360], type: "angle", name: "Hue", }, w: { range: [0, 100], name: "Whiteness", }, b: { range: [0, 100], name: "Blackness", }, }, base: HSV, fromBase (hsv) { let [h, s, v] = hsv; return [h, (v * (100 - s)) / 100, 100 - v]; }, toBase (hwb) { let [h, w, b] = hwb; // Now convert percentages to [0..1] w /= 100; b /= 100; // Achromatic check (white plus black >= 1) let sum = w + b; if (sum >= 1) { let gray = w / sum; return [h, 0, gray * 100]; } let v = 1 - b; let s = v === 0 ? 0 : 1 - w / v; return [h, s * 100, v * 100]; }, formats: { hwb: { coords: [" | ", " | ", " | "], }, }, }); var OKLCHSpace = new ColorSpace({ id: "oklch", name: "OkLCh", coords: { l: { refRange: [0, 1], name: "Lightness", }, c: { refRange: [0, 0.4], name: "Chroma", }, h: { refRange: [0, 360], type: "angle", name: "Hue", }, }, white: "D65", base: OKLab, fromBase: LCHSpace.fromBase, toBase: LCHSpace.toBase, formats: { oklch: { coords: [" | ", " | ", " | "], }, }, }); var HSBSpace = new ColorSpace({ id: 'hsb', name: 'HSB', coords: { h: { refRange: [0, 360], type: 'angle', name: 'Hue' }, s: { range: [0, 100], name: 'Saturation' }, b: { range: [0, 100], name: 'Brightness' } }, base: sRGB, fromBase: rgb => { const val = Math.max(...rgb); const chroma = val - Math.min(...rgb); let [red, green, blue] = rgb; let hue, sat; if (chroma === 0) { // Return early if grayscale. hue = 0; sat = 0; } else { sat = chroma / val; if (red === val) { // Magenta to yellow. hue = (green - blue) / chroma; } else if (green === val) { // Yellow to cyan. hue = 2 + (blue - red) / chroma; } else if (blue === val) { // Cyan to magenta. hue = 4 + (red - green) / chroma; } if (hue < 0) { // Confine hue to the interval [0, 1). hue += 6; } else if (hue >= 6) { hue -= 6; } } return [hue / 6 * 360, sat * 100, val * 100]; }, toBase, formats: { default: { type: 'custom', serialize: (coords, alpha) => { const rgb = toBase(coords); let ret = `rgb(${ Math.round(rgb[0] * 100 * 100) / 100 }% ${ Math.round(rgb[1] * 100 * 100) / 100 }% ${ Math.round(rgb[2] * 100 * 100) / 100 }%`; if (alpha < 1) { ret += ` / ${alpha}`; } ret += ')'; return ret; } }, 'hsb': { coords: [' | ', '', ''] }, 'hsba': { coords: [' | ', '', ''], commans: true, lastAlpha: true } } }); function toBase(hsb){ const hue = hsb[0] / 360 * 6; // We will split hue into 6 sectors. const sat = hsb[1] / 100; const val = hsb[2] / 100; let RGB = []; if (sat === 0) { RGB = [val, val, val]; // Return early if grayscale. } else { const sector = Math.floor(hue); const tint1 = val * (1 - sat); const tint2 = val * (1 - sat * (hue - sector)); const tint3 = val * (1 - sat * (1 + sector - hue)); let red, green, blue; if (sector === 1) { // Yellow to green. red = tint2; green = val; blue = tint1; } else if (sector === 2) { // Green to cyan. red = tint1; green = val; blue = tint3; } else if (sector === 3) { // Cyan to blue. red = tint1; green = tint2; blue = val; } else if (sector === 4) { // Blue to magenta. red = tint3; green = tint1; blue = val; } else if (sector === 5) { // Magenta to red. red = val; green = tint1; blue = tint2; } else { // Red to yellow (sector could be 0 or 6). red = val; green = tint3; blue = tint1; } RGB = [red, green, blue]; } return RGB; } /** * @module Color * @submodule Creating & Reading * @for p5 * @requires core * @requires color_conversion */ const map = (n, start1, stop1, start2, stop2, clamp) => { let result = ((n - start1) / (stop1 - start1) * (stop2 - start2) + start2); if (clamp) { result = Math.max(result, Math.min(start2, stop2)); result = Math.min(result, Math.max(start2, stop2)); } return result; }; const toHexComponent = (v) => { const vInt = ~~(v * 255); const hex = vInt.toString(16); if (hex.length < 2) { return '0' + hex } else { return hex } }; const serializationMap = new Map(); class Color { static colorMap = {}; static #colorjsMaxes = {}; static #grayscaleMap = {}; // Used to add additional color modes to p5.js // Uses underlying library's definition static addColorMode(mode, definition){ ColorSpace.register(definition); Color.colorMap[mode] = definition.id; // Get colorjs maxes Color.#colorjsMaxes[mode] = Object.values(definition.coords) .reduce((acc, v) => { acc.push(v.refRange || v.range); return acc; }, []); Color.#colorjsMaxes[mode].push([0, 1]); // Get grayscale mapping Color.#grayscaleMap[mode] = definition.fromGray; } constructor(vals, colorMode, colorMaxes, { clamp = false } = {}) { // This changes with the color object this._cachedMode = colorMode || RGB; if(vals instanceof Color){ // Received Color object to be used for color mode conversion const mode = colorMode ? Color.colorMap[colorMode] : Color.colorMap[vals.mode]; this._initialize = () => { this._cachedColor = to(vals._color, mode); this._cachedMode = mode; }; }else if (typeof vals === 'object' && !Array.isArray(vals) && vals !== null){ // Received color.js object to be used internally const mode = colorMode ? Color.colorMap[colorMode] : vals.spaceId; this._initialize = () => { this._cachedColor = to(vals, mode); this._cachedMode = colorMode || Object.entries(Color.colorMap) .find(([key, val]) => { return val === this._cachedColor.spaceId; }); }; } else if(typeof vals[0] === 'string') { // Received string this._defaultStringValue = vals[0]; this._initialize = () => { try{ this._cachedColor = parse$4(vals[0]); const [mode] = Object.entries(Color.colorMap).find(([key, val]) => { return val === this._cachedColor.spaceId; }); this._cachedMode = mode; this._cachedColor = to(this._cachedColor, this._cachedColor.spaceId); }catch(err){ // TODO: Invalid color string throw new Error('Invalid color string'); } }; }else { // Received individual channel values let mappedVals; if(colorMaxes){ // NOTE: need to consider different number of arguments (eg. CMYK) if(vals.length === 4){ mappedVals = Color.mapColorRange(vals, this._cachedMode, colorMaxes, clamp); }else if(vals.length === 3){ mappedVals = Color.mapColorRange( [vals[0], vals[1], vals[2]], this._cachedMode, colorMaxes, clamp ); mappedVals.push(1); }else if(vals.length === 2){ // Grayscale with alpha if(Color.#grayscaleMap[this._cachedMode]){ mappedVals = Color.#grayscaleMap[this._cachedMode]( vals[0], colorMaxes, clamp ); }else { mappedVals = Color.mapColorRange( [vals[0], vals[0], vals[0]], this._cachedMode, colorMaxes, clamp ); } const alphaMaxes = Array.isArray(colorMaxes[colorMaxes.length-1]) ? colorMaxes[colorMaxes.length-1] : [0, colorMaxes[colorMaxes.length-1]]; mappedVals.push( map( vals[1], alphaMaxes[0], alphaMaxes[1], 0, 1, clamp ) ); }else if(vals.length === 1){ // Grayscale only if(Color.#grayscaleMap[this._cachedMode]){ mappedVals = Color.#grayscaleMap[this._cachedMode]( vals[0], colorMaxes, clamp ); }else { mappedVals = Color.mapColorRange( [vals[0], vals[0], vals[0]], this._cachedMode, colorMaxes, clamp ); } mappedVals.push(1); }else { throw new Error('Invalid color'); } }else { mappedVals = vals; } if (this._cachedMode === RGB) { if (mappedVals[3] === 1) { // Faster for the browser to parse than rgba this._defaultStringValue = '#' + toHexComponent(mappedVals[0]) + toHexComponent(mappedVals[1]) + toHexComponent(mappedVals[2]); } else { this._defaultStringValue = '#' + toHexComponent(mappedVals[0]) + toHexComponent(mappedVals[1]) + toHexComponent(mappedVals[2]) + toHexComponent(mappedVals[3]); } } this._initialize = () => { const space = Color.colorMap[this._cachedMode] || console.error('Invalid color mode'); const coords = mappedVals.slice(0, 3); const color = { space, coords, alpha: mappedVals[3] }; this._cachedColor = to(color, space); }; } } // Color mode of the Color object, uses p5 color modes get mode() { if (this._initialize) { this._initialize(); this._initialize = undefined; } return this._cachedMode; } // Reference to underlying color object depending on implementation // Not meant to be used publicly unless the implementation is known for sure get _color() { if (this._initialize) { this._initialize(); this._initialize = undefined; } return this._cachedColor; } set _color(newColor) { if (this._initialize) { this._initialize(); this._initialize = undefined; } this._cachedColor = newColor; } // Convert from p5 color range to color.js color range static mapColorRange(origin, mode, maxes, clamp){ const p5Maxes = maxes.map(max => { if(!Array.isArray(max)){ return [0, max]; }else { return max; } }); const colorjsMaxes = Color.#colorjsMaxes[mode]; return origin.map((channel, i) => { const newval = map( channel, p5Maxes[i][0], p5Maxes[i][1], colorjsMaxes[i][0], colorjsMaxes[i][1], clamp ); return newval; }); } // Convert from color.js color range to p5 color range static unmapColorRange(origin, mode, maxes){ const p5Maxes = maxes.map(max => { if(!Array.isArray(max)){ return [0, max]; }else { return max; } }); const colorjsMaxes = Color.#colorjsMaxes[mode]; return origin.map((channel, i) => { const newval = map( channel, colorjsMaxes[i][0], colorjsMaxes[i][1], p5Maxes[i][0], p5Maxes[i][1] ); return newval; }); } // Will do conversion in-Gamut as out of Gamut conversion is only really useful for futher conversions #toColorMode(mode){ return new Color(this._color, mode); } // Get raw coordinates of underlying library, can differ between libraries get _array() { return this._getRGBA(); } array(){ return this._array; } lerp(color, amt, mode){ // Find the closest common ancestor color space let spaceIndex = -1; while( ( spaceIndex+1 < this._color.space.path.length || spaceIndex+1 < color._color.space.path.length ) && this._color.space.path[spaceIndex+1] === color._color.space.path[spaceIndex+1] ){ spaceIndex += 1; } if (spaceIndex === -1) { // This probably will not occur in practice throw new Error('Cannot lerp colors. No common color space found'); } const obj = range(this._color, color._color, { space: this._color.space.path[spaceIndex].id })(amt); return new Color(obj, mode || this.mode); } /** * Returns the color formatted as a `String`. * * Calling `myColor.toString()` can be useful for debugging, as in * `print(myColor.toString())`. It's also helpful for using p5.js with other * libraries. * * The parameter, `format`, is optional. If a format string is passed, as in * `myColor.toString('#rrggbb')`, it will determine how the color string is * formatted. By default, color strings are formatted as `'rgba(r, g, b, a)'`. * * @param {String} [format] how the color string will be formatted. * Leaving this empty formats the string as rgba(r, g, b, a). * '#rgb' '#rgba' '#rrggbb' and '#rrggbbaa' format as hexadecimal color codes. * 'rgb' 'hsb' and 'hsl' return the color formatted in the specified color mode. * 'rgba' 'hsba' and 'hsla' are the same as above but with alpha channels. * 'rgb%' 'hsb%' 'hsl%' 'rgba%' 'hsba%' and 'hsla%' format as percentages. * @return {String} the formatted string. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let myColor = color('darkorchid'); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the text. * text(myColor.toString('#rrggbb'), 50, 50); * * describe('The text "#9932cc" written in purple on a gray background.'); * } */ toString(format) { if (format === undefined && this._defaultStringValue !== undefined) { return this._defaultStringValue; } let outputFormat = format; if (format === '#rrggbb') { outputFormat = 'hex'; } const key = `${this._color.space.id}-${this._color.coords.join(',')}-${this._color.alpha}-${format}`; let colorString = serializationMap.get(key); if (!colorString) { colorString = serialize(this._color, { format: outputFormat }); if (format === '#rrggbb') { colorString = String(colorString); if (colorString.length === 4) { const r = colorString[1]; const g = colorString[2]; const b = colorString[3]; colorString = `#${r}${r}${g}${g}${b}${b}`; } if (colorString.length > 7) { colorString = colorString.slice(0, 7); } colorString = colorString.toLowerCase(); } if (serializationMap.size > 1000) { serializationMap.delete(serializationMap.keys().next().value); } serializationMap.set(key, colorString); } return colorString; } /** * Checks the contrast between two colors. This method returns a boolean * value to indicate if the two color has enough contrast. `true` means that * the colors has enough contrast to be used as background color and body * text color. `false` means there is not enough contrast. * * A second argument can be passed to the method, `options` , which defines * the algorithm to be used. The algorithms currently supported are * WCAG 2.1 (`'WCAG21'`) or APCA (`'APCA'`). The default is WCAG 2.1. If a * value of `'all'` is passed to the `options` argument, an object containing * more details is returned. The details object will include the calculated * contrast value of the colors and different passing criteria. * * For more details about color contrast, you can check out * this page from color.js, and the * WebAIM color contrast checker. * * @param {Color} other * @returns {boolean|object} * @example * let bgColor, fg1Color, fg2Color, msg1, msg2; * function setup() { * createCanvas(100, 100); * bgColor = color(0); * fg1Color = color(100); * fg2Color = color(220); * * if(bgColor.contrast(fg1Color)){ * msg1 = 'good'; * }else{ * msg1 = 'bad'; * } * * if(bgColor.contrast(fg2Color)){ * msg2 = 'good'; * }else{ * msg2 = 'bad'; * } * * describe('A black canvas with a faint grey word saying "bad" at the top left and a brighter light grey word saying "good" in the middle of the canvas.'); * } * * function draw(){ * background(bgColor); * * textSize(18); * * fill(fg1Color); * text(msg1, 10, 30); * * fill(fg2Color); * text(msg2, 10, 60); * } * * @example * let bgColor, fgColor, contrast; * function setup() { * createCanvas(100, 100); * bgColor = color(0); * fgColor = color(200); * contrast = bgColor.contrast(fgColor, 'all'); * * describe('A black canvas with four short lines of grey text that respectively says: "WCAG 2.1", "12.55", "APCA", and "-73.30".'); * } * * function draw(){ * background(bgColor); * * textSize(14); * * fill(fgColor); * text('WCAG 2.1', 10, 25); * text(nf(contrast.WCAG21.value, 0, 2), 10, 40); * * text('APCA', 10, 70); * text(nf(contrast.APCA.value, 0, 2), 10, 85); * } */ contrast(other_color, options='WCAG21') { if(options !== 'all'){ let contrastVal, minimum; switch(options){ case 'WCAG21': contrastVal = contrastWCAG21(this._color, other_color._color); minimum = 4.5; break; case 'APCA': contrastVal = Math.abs(contrastAPCA(this._color, other_color._color)); minimum = 75; break; default: return null; } return contrastVal >= minimum; }else { const wcag21Value = contrastWCAG21(this._color, other_color._color); const apcaValue = contrastAPCA(this._color, other_color._color); return { WCAG21: { value: wcag21Value, passedMinimum: wcag21Value >= 4.5, passedAAA: wcag21Value >= 7 }, APCA: { value: apcaValue, passedMinimum: Math.abs(apcaValue) >= 75 } }; } }; /** * Sets the red component of a color. * * The range depends on the colorMode(). In the * default RGB mode it's between 0 and 255. * * @param {Number} red the new red value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color(255, 128, 128); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 20, 35, 60); * * // Change the red value. * c.setRed(64); * * // Draw the right rectangle. * fill(c); * rect(50, 20, 35, 60); * * describe('Two rectangles. The left one is salmon pink and the right one is teal.'); * } */ setRed(new_red, max=[0, 1]) { this._defaultStringValue = undefined; if(!Array.isArray(max)){ max = [0, max]; } const colorjsMax = Color.#colorjsMaxes[RGB][0]; const newval = map(new_red, max[0], max[1], colorjsMax[0], colorjsMax[1]); if(this.mode === RGB || this.mode === RGBHDR){ this._color.coords[0] = newval; }else { // Will do an imprecise conversion to 'srgb', not recommended const space = this._color.space.id; const representation = to(this._color, 'srgb'); representation.coords[0] = newval; this._color = to(representation, space); } } /** * Sets the green component of a color. * * The range depends on the colorMode(). In the * default RGB mode it's between 0 and 255. * * @param {Number} green the new green value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color(255, 128, 128); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 20, 35, 60); * * // Change the green value. * c.setGreen(255); * * // Draw the right rectangle. * fill(c); * rect(50, 20, 35, 60); * * describe('Two rectangles. The left one is salmon pink and the right one is yellow.'); * } */ setGreen(new_green, max=[0, 1]) { this._defaultStringValue = undefined; if(!Array.isArray(max)){ max = [0, max]; } const colorjsMax = Color.#colorjsMaxes[RGB][1]; const newval = map(new_green, max[0], max[1], colorjsMax[0], colorjsMax[1]); if(this.mode === RGB || this.mode === RGBHDR){ this._color.coords[1] = newval; }else { // Will do an imprecise conversion to 'srgb', not recommended const space = this._color.space.id; const representation = to(this._color, 'srgb'); representation.coords[1] = newval; this._color = to(representation, space); } } /** * Sets the blue component of a color. * * The range depends on the colorMode(). In the * default RGB mode it's between 0 and 255. * * @param {Number} blue the new blue value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color(255, 128, 128); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 20, 35, 60); * * // Change the blue value. * c.setBlue(255); * * // Draw the right rectangle. * fill(c); * rect(50, 20, 35, 60); * * describe('Two rectangles. The left one is salmon pink and the right one is pale fuchsia.'); * } */ setBlue(new_blue, max=[0, 1]) { this._defaultStringValue = undefined; if(!Array.isArray(max)){ max = [0, max]; } const colorjsMax = Color.#colorjsMaxes[RGB][2]; const newval = map(new_blue, max[0], max[1], colorjsMax[0], colorjsMax[1]); if(this.mode === RGB || this.mode === RGBHDR){ this._color.coords[2] = newval; }else { // Will do an imprecise conversion to 'srgb', not recommended const space = this._color.space.id; const representation = to(this._color, 'srgb'); representation.coords[2] = newval; this._color = to(representation, space); } } /** * Sets the alpha (transparency) value of a color. * * The range depends on the * colorMode(). In the default RGB mode it's * between 0 and 255. * * @param {Number} alpha the new alpha value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color(255, 128, 128); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 20, 35, 60); * * // Change the alpha value. * c.setAlpha(128); * * // Draw the right rectangle. * fill(c); * rect(50, 20, 35, 60); * * describe('Two rectangles. The left one is salmon pink and the right one is faded pink.'); * } */ setAlpha(new_alpha, max=[0, 1]) { this._defaultStringValue = undefined; if(!Array.isArray(max)){ max = [0, max]; } const colorjsMax = Color.#colorjsMaxes[this.mode][3]; const newval = map(new_alpha, max[0], max[1], colorjsMax[0], colorjsMax[1]); this._color.alpha = newval; } _getRGBA(maxes=[1, 1, 1, 1]) { // Get colorjs maxes const colorjsMaxes = Color.#colorjsMaxes[RGB]; // Normalize everything to 0,1 or the provided range (map) let coords = structuredClone(to(this._color, 'srgb').coords); coords.push(this._color.alpha); const rangeMaxes = maxes.map((v) => { if(!Array.isArray(v)){ return [0, v]; }else { return v } }); coords = coords.map((coord, i) => { return map( coord, colorjsMaxes[i][0], colorjsMaxes[i][1], rangeMaxes[i][0], rangeMaxes[i][1] ); }); return coords; } _getMode() { return this.mode; } _getRed(max=[0, 1]) { if(!Array.isArray(max)){ max = [0, max]; } if(this.mode === RGB || this.mode === RGBHDR){ const colorjsMax = Color.#colorjsMaxes[this.mode][0]; return map( this._color.coords[0], colorjsMax[0], colorjsMax[1], max[0], max[1] ); }else { // Will do an imprecise conversion to 'srgb', not recommended const colorjsMax = Color.#colorjsMaxes[RGB][0]; return map(to(this._color, 'srgb').coords[0], colorjsMax[0], colorjsMax[1], max[0], max[1]); } } /** * This function extracts the green value from a color object and * returns it in the range 0–255 by default. When `colorMode()` is given to an * RBG value, the green value within the givin range is returned */ _getGreen(max=[0, 1]) { if(!Array.isArray(max)){ max = [0, max]; } if(this.mode === RGB || this.mode === RGBHDR){ const colorjsMax = Color.#colorjsMaxes[this.mode][1]; return map( this._color.coords[1], colorjsMax[0], colorjsMax[1], max[0], max[1] ); }else { // Will do an imprecise conversion to 'srgb', not recommended const colorjsMax = Color.#colorjsMaxes[RGB][1]; return map(to(this._color, 'srgb').coords[1], colorjsMax[0], colorjsMax[1], max[0], max[1]); } } _getBlue(max=[0, 1]) { if(!Array.isArray(max)){ max = [0, max]; } if(this.mode === RGB || this.mode === RGBHDR){ const colorjsMax = Color.#colorjsMaxes[this.mode][2]; return map( this._color.coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1] ); }else { // Will do an imprecise conversion to 'srgb', not recommended const colorjsMax = Color.#colorjsMaxes[RGB][2]; return map(to(this._color, 'srgb').coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1]); } } _getAlpha(max=[0, 1]) { if(!Array.isArray(max)){ max = [0, max]; } const colorjsMax = Color.#colorjsMaxes[this.mode][3]; return map(this._color.alpha, colorjsMax[0], colorjsMax[1], max[0], max[1]); } /** * Hue is the same in HSB and HSL, but the maximum value may be different. * This function will return the HSB-normalized saturation when supplied with * an HSB color object, but will default to the HSL-normalized saturation * otherwise. */ _getHue(max=[0, 360]) { if(!Array.isArray(max)){ max = [0, max]; } if(this.mode === HSB || this.mode === HSL){ const colorjsMax = Color.#colorjsMaxes[this.mode][0]; return map( this._color.coords[0], colorjsMax[0], colorjsMax[1], max[0], max[1] ); }else { // Will do an imprecise conversion to 'HSL', not recommended const colorjsMax = Color.#colorjsMaxes[HSL][0]; return map(to(this._color, 'hsl').coords[0], colorjsMax[0], colorjsMax[1], max[0], max[1]); } } /** * Saturation is scaled differently in HSB and HSL. This function will return * the HSB saturation when supplied with an HSB color object, but will default * to the HSL saturation otherwise. */ _getSaturation(max=[0, 100]) { if(!Array.isArray(max)){ max = [0, max]; } if(this.mode === HSB || this.mode === HSL){ const colorjsMax = Color.#colorjsMaxes[this.mode][1]; return map( this._color.coords[1], colorjsMax[0], colorjsMax[1], max[0], max[1] ); }else { // Will do an imprecise conversion to 'HSL', not recommended const colorjsMax = Color.#colorjsMaxes[HSL][1]; return map(to(this._color, 'hsl').coords[1], colorjsMax[0], colorjsMax[1], max[0], max[1]); } } /** * Brightness obtains the HSB brightness value from either a p5.Color object, * an array of color components, or a CSS color string.Depending on value, * when `colorMode()` is set to HSB, this function will return the * brightness value in the range. By default, this function will return * the HSB brightness within the range 0 - 100. */ _getBrightness(max=[0, 100]) { if(!Array.isArray(max)){ max = [0, max]; } if(this.mode === HSB){ const colorjsMax = Color.#colorjsMaxes[this.mode][2]; return map( this._color.coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1] ); }else { // Will do an imprecise conversion to 'HSB', not recommended const colorjsMax = Color.#colorjsMaxes[HSB][2]; return map(to(this._color, 'hsb').coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1]); } } _getLightness(max=[0, 100]) { if(!Array.isArray(max)){ max = [0, max]; } if(this.mode === HSL){ const colorjsMax = Color.#colorjsMaxes[this.mode][2]; return map( this._color.coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1] ); }else { // Will do an imprecise conversion to 'HSL', not recommended const colorjsMax = Color.#colorjsMaxes[HSL][2]; return map(to(this._color, 'hsl').coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1]); } } } function color$1(p5, fn, lifecycles){ /** * A class to describe a color. * * Each `p5.Color` object stores the color mode * and level maxes that were active during its construction. These values are * used to interpret the arguments passed to the object's constructor. They * also determine output formatting such as when * saturation() is called. * * Color is stored internally as an array of ideal RGBA values in floating * point form, normalized from 0 to 1. These values are used to calculate the * closest screen colors, which are RGBA levels from 0 to 255. Screen colors * are sent to the renderer. * * When different color representations are calculated, the results are cached * for performance. These values are normalized, floating-point numbers. * * Note: color() is the recommended way to create an * instance of this class. * * @class p5.Color * @param {p5} pInst pointer to p5 instance. * * @param {Number[]|String} vals an array containing the color values * for red, green, blue and alpha channel * or CSS color. */ /** * @class p5.Color * @param {Number[]|String} vals */ p5.Color = Color; sRGB.fromGray = P3.fromGray = function(val, maxes, clamp){ // Use blue max const p5Maxes = maxes.map(max => { if(!Array.isArray(max)){ return [0, max]; }else { return max; } }); const v = map(val, p5Maxes[2][0], p5Maxes[2][1], 0, 1, clamp); return [v, v, v]; }; HSBSpace.fromGray = HSLSpace.fromGray = function(val, maxes, clamp){ // Use brightness max const p5Maxes = maxes.map(max => { if(!Array.isArray(max)){ return [0, max]; }else { return max; } }); const v = map(val, p5Maxes[2][0], p5Maxes[2][1], 0, 100, clamp); return [0, 0, v]; }; HWBSpace.fromGray = function(val, maxes, clamp){ // Use Whiteness and Blackness to create number line const p5Maxes = maxes.map(max => { if(!Array.isArray(max)){ return [0, max]; }else { return max; } }); const wbMax = (Math.abs(p5Maxes[1][0] - p5Maxes[1][1])) / 2 + (Math.abs(p5Maxes[2][0] - p5Maxes[2][1])) / 2; const nVal = map(val, 0, wbMax, 0, 100); let white, black; if(nVal < 50){ black = nVal; white = 100 - nVal; }else if(nVal >= 50){ white = nVal; black = 100 - nVal; } return [0, white, black]; }; Lab.fromGray = LCHSpace.fromGray = OKLab.fromGray = OKLCHSpace.fromGray = function(val, maxes, clamp){ // Use lightness max const p5Maxes = maxes.map(max => { if(!Array.isArray(max)){ return [0, max]; }else { return max; } }); const v = map(val, p5Maxes[0][0], p5Maxes[0][1], 0, 100, clamp); return [v, 0, 0]; }; // Register color modes and initialize Color maxes to what p5 has set for itself p5.Color.addColorMode(RGB, sRGB); p5.Color.addColorMode(RGBHDR, P3); p5.Color.addColorMode(HSB, HSBSpace); p5.Color.addColorMode(HSL, HSLSpace); p5.Color.addColorMode(HWB, HWBSpace); p5.Color.addColorMode(LAB, Lab); p5.Color.addColorMode(LCH, LCHSpace); p5.Color.addColorMode(OKLAB, OKLab); p5.Color.addColorMode(OKLCH, OKLCHSpace); lifecycles.presetup = function(){ const pInst = this; // Decorate set methods const setMethods = ['Red', 'Green', 'Blue', 'Alpha']; for(let i in setMethods){ const method = setMethods[i]; const setCopy = p5.Color.prototype['set' + method]; p5.Color.prototype['set' + method] = function(newval, max){ max = max || pInst?._renderer?.states?.colorMaxes?.[RGB][i]; return setCopy.call(this, newval, max); }; } // Decorate get methods function decorateGet(channel, modes){ const getCopy = p5.Color.prototype['_get' + channel]; p5.Color.prototype['_get' + channel] = function(max){ if(Object.keys(modes).includes(this.mode)){ max = max || pInst?._renderer?.states?.colorMaxes?.[this.mode][modes[this.mode]]; }else { const defaultMode = Object.keys(modes)[0]; max = max || pInst ?._renderer ?.states ?.colorMaxes ?.[defaultMode][modes[defaultMode]]; } return getCopy.call(this, max); }; } decorateGet('Red', { [RGB]: 0, [RGBHDR]: 0 }); decorateGet('Green', { [RGB]: 1, [RGBHDR]: 1 }); decorateGet('Blue', { [RGB]: 2, [RGBHDR]: 2 }); decorateGet('Alpha', { [RGB]: 3, [RGBHDR]: 3, [HSB]: 3, [HSL]: 3, [HWB]: 3, [LAB]: 3, [LCH]: 3, [OKLAB]: 3, [OKLCH]: 3 }); decorateGet('Hue', { [HSL]: 0, [HSB]: 0, [HWB]: 0, [LCH]: 2, [OKLCH]: 2 }); decorateGet('Saturation', { [HSL]: 1, [HSB]: 1 }); decorateGet('Brightness', { [HSB]: 2 }); decorateGet('Lightness', { [HSL]: 2 }); }; } if(typeof p5 !== 'undefined'){ color$1(p5); } /** * @module Color * @submodule Creating & Reading * @for p5 * @requires core * @requires constants */ /** * @typedef {'rgb'} RGB * @property {RGB} RGB * @final */ const RGB = 'rgb'; /** * @typedef {'rgbhdr'} RGBHDR * @property {RGBHDR} RGBHDR * @final */ const RGBHDR = 'rgbhdr'; /** * HSB (hue, saturation, brightness) is a type of color model. * You can learn more about it at * HSB. * * @typedef {'hsb'} HSB * @property {HSB} HSB * @final */ const HSB = 'hsb'; /** * @typedef {'hsl'} HSL * @property {HSL} HSL * @final */ const HSL = 'hsl'; /** * @typedef {'hwb'} HWB * @property {HWB} HWB * @final */ const HWB = 'hwb'; /** * @typedef {'lab'} LAB * @property {LAB} LAB * @final */ const LAB = 'lab'; /** * @typedef {'lch'} LCH * @property {LCH} LCH * @final */ const LCH = 'lch'; /** * @typedef {'oklab'} OKLAB * @property {OKLAB} OKLAB * @final */ const OKLAB = 'oklab'; /** * @typedef {'oklch'} OKLCH * @property {OKLCH} OKLCH * @final */ const OKLCH = 'oklch'; /** * @typedef {'rgba'} RGBA * @property {RGBA} RGBA * @final */ const RGBA = 'rgba'; function creatingReading(p5, fn){ fn.RGB = RGB; fn.RGBHDR = RGBHDR; fn.HSB = HSB; fn.HSL = HSL; fn.HWB = HWB; fn.LAB = LAB; fn.LCH = LCH; fn.OKLAB = OKLAB; fn.OKLCH = OKLCH; fn.RGBA = RGBA; // Add color states to renderer state machine p5.Renderer.states.colorMode = RGB; p5.Renderer.states.colorMaxes = { [RGB]: [255, 255, 255, 255], [RGBHDR]: [255, 255, 255, 255], [HSB]: [360, 100, 100, 1], [HSL]: [360, 100, 100, 1], [HWB]: [360, 100, 100, 1], [LAB]: [100, [-125, 125], [-125, 125], 1], [LCH]: [100, 150, 360, 1], [OKLAB]: [100, [-125, 125], [-125, 125], 1], [OKLCH]: [100, 150, 360, 1], clone: function(){ const cloned = { ...this }; for (const key in cloned) { if (cloned[key] instanceof Array) { cloned[key] = [...cloned[key]]; } } return cloned; } }; /** * Creates a p5.Color object. * * By default, the parameters are interpreted as RGB values. Calling * `color(255, 204, 0)` will return a bright yellow color. The way these * parameters are interpreted may be changed with the * colorMode() function. * * The version of `color()` with one parameter interprets the value one of two * ways. If the parameter is a number, it's interpreted as a grayscale value. * If the parameter is a string, it's interpreted as a CSS color string. * * The version of `color()` with two parameters interprets the first one as a * grayscale value. The second parameter sets the alpha (transparency) value. * * The version of `color()` with three parameters interprets them as RGB, HSB, * or HSL colors, depending on the current `colorMode()`. * * The version of `color()` with four parameters interprets them as RGBA, HSBA, * or HSLA colors, depending on the current `colorMode()`. The last parameter * sets the alpha (transparency) value. * * @method color * @param {Number} gray number specifying value between white and black. * @param {Number} [alpha] alpha value relative to current color range * (default is 0-255). * @return {p5.Color} resulting color. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using RGB values. * let c = color(255, 204, 0); * * // Draw the square. * fill(c); * noStroke(); * square(30, 20, 55); * * describe('A yellow square on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using RGB values. * let c1 = color(255, 204, 0); * * // Draw the left circle. * fill(c1); * noStroke(); * circle(25, 25, 80); * * // Create a p5.Color object using a grayscale value. * let c2 = color(65); * * // Draw the right circle. * fill(c2); * circle(75, 75, 80); * * describe( * 'Two circles on a gray canvas. The circle in the top-left corner is yellow and the one at the bottom-right is gray.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using a named color. * let c = color('magenta'); * * // Draw the square. * fill(c); * noStroke(); * square(20, 20, 60); * * describe('A magenta square on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using a hex color code. * let c1 = color('#0f0'); * * // Draw the left rectangle. * fill(c1); * noStroke(); * rect(0, 10, 45, 80); * * // Create a p5.Color object using a hex color code. * let c2 = color('#00ff00'); * * // Draw the right rectangle. * fill(c2); * rect(55, 10, 45, 80); * * describe('Two bright green rectangles on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using a RGB color string. * let c1 = color('rgb(0, 0, 255)'); * * // Draw the top-left square. * fill(c1); * square(10, 10, 35); * * // Create a p5.Color object using a RGB color string. * let c2 = color('rgb(0%, 0%, 100%)'); * * // Draw the top-right square. * fill(c2); * square(55, 10, 35); * * // Create a p5.Color object using a RGBA color string. * let c3 = color('rgba(0, 0, 255, 1)'); * * // Draw the bottom-left square. * fill(c3); * square(10, 55, 35); * * // Create a p5.Color object using a RGBA color string. * let c4 = color('rgba(0%, 0%, 100%, 1)'); * * // Draw the bottom-right square. * fill(c4); * square(55, 55, 35); * * describe('Four blue squares in the corners of a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using a HSL color string. * let c1 = color('hsl(160, 100%, 50%)'); * * // Draw the left rectangle. * noStroke(); * fill(c1); * rect(0, 10, 45, 80); * * // Create a p5.Color object using a HSLA color string. * let c2 = color('hsla(160, 100%, 50%, 0.5)'); * * // Draw the right rectangle. * fill(c2); * rect(55, 10, 45, 80); * * describe('Two sea green rectangles. A darker rectangle on the left and a brighter one on the right.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using a HSB color string. * let c1 = color('hsb(160, 100%, 50%)'); * * // Draw the left rectangle. * noStroke(); * fill(c1); * rect(0, 10, 45, 80); * * // Create a p5.Color object using a HSBA color string. * let c2 = color('hsba(160, 100%, 50%, 0.5)'); * * // Draw the right rectangle. * fill(c2); * rect(55, 10, 45, 80); * * describe('Two green rectangles. A darker rectangle on the left and a brighter one on the right.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using RGB values. * let c1 = color(50, 55, 100); * * // Draw the left rectangle. * fill(c1); * rect(0, 10, 45, 80); * * // Switch the color mode to HSB. * colorMode(HSB, 100); * * // Create a p5.Color object using HSB values. * let c2 = color(50, 55, 100); * * // Draw the right rectangle. * fill(c2); * rect(55, 10, 45, 80); * * describe('Two blue rectangles. A darker rectangle on the left and a brighter one on the right.'); * } */ /** * @method color * @param {Number} v1 red or hue value relative to * the current color range. * @param {Number} v2 green or saturation value * relative to the current color range. * @param {Number} v3 blue or brightness value * relative to the current color range. * @param {Number} [alpha] * @return {p5.Color} */ /** * @method color * @param {String} value a color string. * @return {p5.Color} */ /** * @method color * @param {Number[]} values an array containing the red, green, blue, * and alpha components of the color. * @return {p5.Color} */ /** * @method color * @param {p5.Color} color * @return {p5.Color} */ fn.color = function(...args) { // p5._validateParameters('color', args); if (args[0] instanceof Color) { // TODO: perhaps change color mode to match instance mode? return args[0]; // Do nothing if argument is already a color object. } const arg = Array.isArray(args[0]) ? args[0] : args; return new Color( arg, this._renderer.states.colorMode, this._renderer.states.colorMaxes[this._renderer.states.colorMode], { clamp: true } ); }; /** * Gets the red value of a color. * * `red()` extracts the red value from a * p5.Color object, an array of color components, or * a CSS color string. * * By default, `red()` returns a color's red value in the range 0 * to 255. If the colorMode() is set to RGB, it * returns the red value in the given range. * * @method red * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the red value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color(175, 100, 220); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'redValue' to 175. * let redValue = red(c); * * // Draw the right rectangle. * fill(redValue, 0, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is red.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a color array. * let c = [175, 100, 220]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'redValue' to 175. * let redValue = red(c); * * // Draw the right rectangle. * fill(redValue, 0, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is red.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a CSS color string. * let c = 'rgb(175, 100, 220)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'redValue' to 175. * let redValue = red(c); * * // Draw the right rectangle. * fill(redValue, 0, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is red.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use RGB color with values in the range 0-100. * colorMode(RGB, 100); * * // Create a p5.Color object. * let c = color(69, 39, 86); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'redValue' to 69. * let redValue = red(c); * * // Draw the right rectangle. * fill(redValue, 0, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is red.'); * } */ fn.red = function(c) { // p5._validateParameters('red', arguments); // Get current red max return this.color(c)._getRed(); }; /** * Gets the green value of a color. * * `green()` extracts the green value from a * p5.Color object, an array of color components, or * a CSS color string. * * By default, `green()` returns a color's green value in the range 0 * to 255. If the colorMode() is set to RGB, it * returns the green value in the given range. * * @method green * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the green value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color(175, 100, 220); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'greenValue' to 100. * let greenValue = green(c); * * // Draw the right rectangle. * fill(0, greenValue, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is dark green.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a color array. * let c = [175, 100, 220]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'greenValue' to 100. * let greenValue = green(c); * * // Draw the right rectangle. * fill(0, greenValue, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is dark green.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a CSS color string. * let c = 'rgb(175, 100, 220)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'greenValue' to 100. * let greenValue = green(c); * * // Draw the right rectangle. * fill(0, greenValue, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is dark green.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use RGB color with values in the range 0-100. * colorMode(RGB, 100); * * // Create a p5.Color object using RGB values. * let c = color(69, 39, 86); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'greenValue' to 39. * let greenValue = green(c); * * // Draw the right rectangle. * fill(0, greenValue, 0); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is dark green.'); * } */ fn.green = function(c) { // p5._validateParameters('green', arguments); // Get current green max return this.color(c)._getGreen(); }; /** * Gets the blue value of a color. * * `blue()` extracts the blue value from a * p5.Color object, an array of color components, or * a CSS color string. * * By default, `blue()` returns a color's blue value in the range 0 * to 255. If the colorMode() is set to RGB, it * returns the blue value in the given range. * * @method blue * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the blue value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object using RGB values. * let c = color(175, 100, 220); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'blueValue' to 220. * let blueValue = blue(c); * * // Draw the right rectangle. * fill(0, 0, blueValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is royal blue.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a color array. * let c = [175, 100, 220]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'blueValue' to 220. * let blueValue = blue(c); * * // Draw the right rectangle. * fill(0, 0, blueValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is royal blue.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a CSS color string. * let c = 'rgb(175, 100, 220)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'blueValue' to 220. * let blueValue = blue(c); * * // Draw the right rectangle. * fill(0, 0, blueValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is royal blue.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use RGB color with values in the range 0-100. * colorMode(RGB, 100); * * // Create a p5.Color object using RGB values. * let c = color(69, 39, 86); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'blueValue' to 86. * let blueValue = blue(c); * * // Draw the right rectangle. * fill(0, 0, blueValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light purple and the right one is royal blue.'); * } */ fn.blue = function(c) { // p5._validateParameters('blue', arguments); // Get current blue max return this.color(c)._getBlue(); }; /** * Gets the alpha (transparency) value of a color. * * `alpha()` extracts the alpha value from a * p5.Color object, an array of color components, or * a CSS color string. * * @method alpha * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the alpha value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color(0, 126, 255, 102); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'alphaValue' to 102. * let alphaValue = alpha(c); * * // Draw the right rectangle. * fill(alphaValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light blue and the right one is charcoal gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a color array. * let c = [0, 126, 255, 102]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'alphaValue' to 102. * let alphaValue = alpha(c); * * // Draw the left rectangle. * fill(alphaValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light blue and the right one is charcoal gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a CSS color string. * let c = 'rgba(0, 126, 255, 0.4)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'alphaValue' to 102. * let alphaValue = alpha(c); * * // Draw the right rectangle. * fill(alphaValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is light blue and the right one is charcoal gray.'); * } */ fn.alpha = function(c) { // p5._validateParameters('alpha', arguments); // Get current alpha max return this.color(c)._getAlpha(); }; /** * Gets the hue value of a color. * * `hue()` extracts the hue value from a * p5.Color object, an array of color components, or * a CSS color string. * * Hue describes a color's position on the color wheel. By default, `hue()` * returns a color's HSL hue in the range 0 to 360. If the * colorMode() is set to HSB or HSL, it returns the hue * value in the given mode. * * @method hue * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the hue value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSL color. * colorMode(HSL); * * // Create a p5.Color object. * let c = color(0, 50, 100); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 20, 35, 60); * * // Set 'hueValue' to 0. * let hueValue = hue(c); * * // Draw the right rectangle. * fill(hueValue); * rect(50, 20, 35, 60); * * describe( * 'Two rectangles. The rectangle on the left is salmon pink and the one on the right is black.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSL color. * colorMode(HSL); * * // Create a color array. * let c = [0, 50, 100]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 20, 35, 60); * * // Set 'hueValue' to 0. * let hueValue = hue(c); * * // Draw the right rectangle. * fill(hueValue); * rect(50, 20, 35, 60); * * describe( * 'Two rectangles. The rectangle on the left is salmon pink and the one on the right is black.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSL color. * colorMode(HSL); * * // Create a CSS color string. * let c = 'rgb(255, 128, 128)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 20, 35, 60); * * // Set 'hueValue' to 0. * let hueValue = hue(c); * * // Draw the right rectangle. * fill(hueValue); * rect(50, 20, 35, 60); * * describe( * 'Two rectangles. The rectangle on the left is salmon pink and the one on the right is black.' * ); * } */ fn.hue = function(c) { let colorMode = HSL; let i = 0; if( this._renderer.states.colorMode === HSB || this._renderer.states.colorMode === HSL ){ colorMode = this._renderer.states.colorMode; }else if( this._renderer.states.colorMode === LCH || this._renderer.states.colorMode === OKLCH ){ colorMode = this._renderer.states.colorMode; i = 2; } return this.color(c)._getHue( this._renderer.states.colorMaxes[colorMode][i] ); }; /** * Gets the saturation value of a color. * * `saturation()` extracts the saturation value from a * p5.Color object, an array of color components, or * a CSS color string. * * Saturation is scaled differently in HSB and HSL. By default, `saturation()` * returns a color's HSL saturation in the range 0 to 100. If the * colorMode() is set to HSB or HSL, it returns the * saturation value in the given mode. * * @method saturation * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the saturation value * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSB color. * colorMode(HSB); * * // Create a p5.Color object. * let c = color(0, 50, 100); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'satValue' to 50. * let satValue = saturation(c); * * // Draw the right rectangle. * fill(satValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is dark gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSB color. * colorMode(HSB); * * // Create a color array. * let c = [0, 50, 100]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'satValue' to 100. * let satValue = saturation(c); * * // Draw the right rectangle. * fill(satValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSB color. * colorMode(HSB); * * // Create a CSS color string. * let c = 'rgb(255, 128, 128)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'satValue' to 100. * let satValue = saturation(c); * * // Draw the right rectangle. * fill(satValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSL color. * colorMode(HSL); * * // Create a p5.Color object. * let c = color(0, 100, 75); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'satValue' to 100. * let satValue = saturation(c); * * // Draw the right rectangle. * fill(satValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is white.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSL color with values in the range 0-255. * colorMode(HSL, 255); * * // Create a p5.Color object. * let c = color(0, 255, 191.5); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'satValue' to 255. * let satValue = saturation(c); * * // Draw the right rectangle. * fill(satValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is white.'); * } */ fn.saturation = function(c) { const colorMode = (this._renderer.states.colorMode === HSB) ? HSB : HSL; return this.color(c)._getSaturation( this._renderer.states.colorMaxes[colorMode][1] ); }; /** * Gets the brightness value of a color. * * `brightness()` extracts the HSB brightness value from a * p5.Color object, an array of color components, or * a CSS color string. * * By default, `brightness()` returns a color's HSB brightness in the range 0 * to 100. If the colorMode() is set to HSB, it * returns the brightness value in the given range. * * @method brightness * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the brightness value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSB color. * colorMode(HSB); * * // Create a p5.Color object. * let c = color(0, 50, 100); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'brightValue' to 100. * let brightValue = brightness(c); * * // Draw the right rectangle. * fill(brightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is white.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSB color. * colorMode(HSB); * * // Create a color array. * let c = [0, 50, 100]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'brightValue' to 100. * let brightValue = brightness(c); * * // Draw the right rectangle. * fill(brightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is white.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSB color. * colorMode(HSB); * * // Create a CSS color string. * let c = 'rgb(255, 128, 128)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'brightValue' to 100. * let brightValue = brightness(c); * * // Draw the right rectangle. * fill(brightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is white.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSB color with values in the range 0-255. * colorMode(HSB, 255); * * // Create a p5.Color object. * let c = color(0, 127, 255); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'brightValue' to 255. * let brightValue = brightness(c); * * // Draw the right rectangle. * fill(brightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is white.'); * } */ fn.brightness = function(c) { return this.color(c)._getBrightness( this._renderer.states.colorMaxes.hsb[2] ); }; /** * Gets the lightness value of a color. * * `lightness()` extracts the HSL lightness value from a * p5.Color object, an array of color components, or * a CSS color string. * * By default, `lightness()` returns a color's HSL lightness in the range 0 * to 100. If the colorMode() is set to HSL, it * returns the lightness value in the given range. * * @method lightness * @param {p5.Color|Number[]|String} color p5.Color object, array of * color components, or CSS color string. * @return {Number} the lightness value. * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSL color. * colorMode(HSL); * * // Create a p5.Color object using HSL values. * let c = color(0, 100, 75); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'lightValue' to 75. * let lightValue = lightness(c); * * // Draw the right rectangle. * fill(lightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSL color. * colorMode(HSL); * * // Create a color array. * let c = [0, 100, 75]; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'lightValue' to 75. * let lightValue = lightness(c); * * // Draw the right rectangle. * fill(lightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSL color. * colorMode(HSL); * * // Create a CSS color string. * let c = 'rgb(255, 128, 128)'; * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'lightValue' to 75. * let lightValue = lightness(c); * * // Draw the right rectangle. * fill(lightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is gray.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use HSL color with values in the range 0-255. * colorMode(HSL, 255); * * // Create a p5.Color object using HSL values. * let c = color(0, 255, 191.5); * * // Draw the left rectangle. * noStroke(); * fill(c); * rect(15, 15, 35, 70); * * // Set 'lightValue' to 191.5. * let lightValue = lightness(c); * * // Draw the right rectangle. * fill(lightValue); * rect(50, 15, 35, 70); * * describe('Two rectangles. The left one is salmon pink and the right one is gray.'); * } */ fn.lightness = function(c) { return this.color(c)._getLightness( this._renderer.states.colorMaxes.hsl[2] ); }; /** * Blends two colors to find a third color between them. * * The `amt` parameter specifies the amount to interpolate between the two * values. 0 is equal to the first color, 0.1 is very near the first color, * 0.5 is halfway between the two colors, and so on. Negative numbers are set * to 0. Numbers greater than 1 are set to 1. This differs from the behavior of * lerp. It's necessary because numbers outside of the * interval [0, 1] will produce strange and unexpected colors. * * The way that colors are interpolated depends on the current * colorMode(). * * @method lerpColor * @param {p5.Color} c1 interpolate from this color. * @param {p5.Color} c2 interpolate to this color. * @param {Number} amt number between 0 and 1. * @return {p5.Color} interpolated color. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create p5.Color objects to interpolate between. * let from = color(218, 165, 32); * let to = color(72, 61, 139); * * // Create intermediate colors. * let interA = lerpColor(from, to, 0.33); * let interB = lerpColor(from, to, 0.66); * * // Draw the left rectangle. * noStroke(); * fill(from); * rect(10, 20, 20, 60); * * // Draw the left-center rectangle. * fill(interA); * rect(30, 20, 20, 60); * * // Draw the right-center rectangle. * fill(interB); * rect(50, 20, 20, 60); * * // Draw the right rectangle. * fill(to); * rect(70, 20, 20, 60); * * describe( * 'Four rectangles. From left to right, the rectangles are tan, brown, brownish purple, and purple.' * ); * } */ fn.lerpColor = function(c1, c2, amt) { // p5._validateParameters('lerpColor', arguments); return c1.lerp(c2, amt, this._renderer.states.colorMode); }; /** * Blends multiple colors to find a color between them. * * The `amt` parameter specifies the amount to interpolate between the color * stops which are colors at each `amt` value "location" with `amt` values * that are between 2 color stops interpolating between them based on its relative * distance to both. * * The way that colors are interpolated depends on the current * colorMode(). * * @method paletteLerp * @param {Array<[(p5.Color|String|Number|Number[]), Number]>} colors_stops color stops to interpolate from * @param {Number} amt number to use to interpolate relative to color stops * @return {p5.Color} interpolated color. * * @example * function setup() { * createCanvas(400, 400); * } * * function draw() { * // The background goes from white to red to green to blue fill * background(paletteLerp([ * ['white', 0], * ['red', 0.05], * ['green', 0.25], * ['blue', 1] * ], millis() / 10000 % 1)); * } */ fn.paletteLerp = function(color_stops, amt) { const first_color_stop = color_stops[0]; if (amt < first_color_stop[1]) return this.color(first_color_stop[0]); for (let i = 1; i < color_stops.length; i++) { const color_stop = color_stops[i]; if (amt < color_stop[1]) { const prev_color_stop = color_stops[i - 1]; return this.lerpColor( this.color(prev_color_stop[0]), this.color(color_stop[0]), (amt - prev_color_stop[1]) / (color_stop[1] - prev_color_stop[1]) ); } } return this.color(color_stops[color_stops.length - 1][0]); }; } if(typeof p5 !== 'undefined'){ creatingReading(p5, p5.prototype); } /** * @module IO * @submodule Input * @requires core */ class XML { constructor(DOM){ if (!DOM) { const xmlDoc = document.implementation.createDocument(null, 'doc'); this.DOM = xmlDoc.createElement('root'); } else { this.DOM = DOM; } } /** * Returns the element's parent element as a new p5.XML * object. * * @return {p5.XML} parent element. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get an array with all mammal elements. * let mammals = myXML.getChildren('mammal'); * * // Get the first mammal element. * let firstMammal = mammals[0]; * * // Get the parent element. * let parent = firstMammal.getParent(); * * // Get the parent element's name. * let name = parent.getName(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the parent element's name. * text(name, 50, 50); * * describe('The word "animals" written in black on a gray background.'); * } */ getParent() { return new XML(this.DOM.parentElement); } /** * Returns the element's name as a `String`. * * An XML element's name is given by its tag. For example, the element * `<language>JavaScript</language>` has the name `language`. * * @return {String} name of the element. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get an array with all mammal elements. * let mammals = myXML.getChildren('mammal'); * * // Get the first mammal element. * let firstMammal = mammals[0]; * * // Get the mammal element's name. * let name = firstMammal.getName(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the element's name. * text(name, 50, 50); * * describe('The word "mammal" written in black on a gray background.'); * } */ getName() { return this.DOM.tagName; } /** * Sets the element's tag name. * * An XML element's name is given by its tag. For example, the element * `<language>JavaScript</language>` has the name `language`. * * The parameter, `name`, is the element's new name as a string. For example, * calling `myXML.setName('planet')` will make the element's new tag name * `<planet></planet>`. * * @param {String} name new tag name of the element. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the element's original name. * let oldName = myXML.getName(); * * // Set the element's name. * myXML.setName('monsters'); * * // Get the element's new name. * let newName = myXML.getName(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the element's names. * text(oldName, 50, 33); * text(newName, 50, 67); * * describe( * 'The words "animals" and "monsters" written on separate lines. The text is black on a gray background.' * ); * } */ setName(name) { const content = this.DOM.innerHTML; const attributes = this.DOM.attributes; const xmlDoc = document.implementation.createDocument(null, 'default'); const newDOM = xmlDoc.createElement(name); newDOM.innerHTML = content; for (let i = 0; i < attributes.length; i++) { newDOM.setAttribute(attributes[i].nodeName, attributes[i].nodeValue); } this.DOM = newDOM; } /** * Returns `true` if the element has child elements and `false` if not. * * @return {boolean} whether the element has children. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Check whether the element has child elements. * let isParent = myXML.hasChildren(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Style the text. * if (isParent === true) { * text('Parent', 50, 50); * } else { * text('Not Parent', 50, 50); * } * * describe('The word "Parent" written in black on a gray background.'); * } */ hasChildren() { return this.DOM.children.length > 0; } /** * Returns an array with the names of the element's child elements as * `String`s. * * @return {String[]} names of the child elements. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the names of the element's children as an array. * let children = myXML.listChildren(); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Iterate over the array. * for (let i = 0; i < children.length; i += 1) { * * // Calculate the y-coordinate. * let y = (i + 1) * 25; * * // Display the child element's name. * text(children[i], 10, y); * } * * describe( * 'The words "mammal", "mammal", "mammal", and "reptile" written on separate lines. The text is black on a gray background.' * ); * } */ listChildren() { const arr = []; for (let i = 0; i < this.DOM.childNodes.length; i++) { arr.push(this.DOM.childNodes[i].nodeName); } return arr; } /** * Returns an array with the element's child elements as new * p5.XML objects. * * The parameter, `name`, is optional. If a string is passed, as in * `myXML.getChildren('cat')`, then the method will only return child elements * with the tag `<cat>`. * * @param {String} [name] name of the elements to return. * @return {p5.XML[]} child elements. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get an array of the child elements. * let children = myXML.getChildren(); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Iterate over the array. * for (let i = 0; i < children.length; i += 1) { * * // Calculate the y-coordinate. * let y = (i + 1) * 20; * * // Get the child element's content. * let content = children[i].getContent(); * * // Display the child element's content. * text(content, 10, y); * } * * describe( * 'The words "Goat", "Leopard", "Zebra", and "Turtle" written on separate lines. The text is black on a gray background.' * ); * } * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get an array of the child elements * // that are mammals. * let children = myXML.getChildren('mammal'); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Iterate over the array. * for (let i = 0; i < children.length; i += 1) { * * // Calculate the y-coordinate. * let y = (i + 1) * 20; * * // Get the child element's content. * let content = children[i].getContent(); * * // Display the child element's content. * text(content, 10, y); * } * * describe( * 'The words "Goat", "Leopard", and "Zebra" written on separate lines. The text is black on a gray background.' * ); * } */ getChildren(param) { if (param) { return elementsToP5XML(this.DOM.getElementsByTagName(param)); } else { return elementsToP5XML(this.DOM.children); } } /** * Returns the first matching child element as a new * p5.XML object. * * The parameter, `name`, is optional. If a string is passed, as in * `myXML.getChild('cat')`, then the first child element with the tag * `<cat>` will be returned. If a number is passed, as in * `myXML.getChild(1)`, then the child element at that index will be returned. * * @param {String|Integer} name element name or index. * @return {p5.XML} child element. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first child element that is a mammal. * let goat = myXML.getChild('mammal'); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Get the child element's content. * let content = goat.getContent(); * * // Display the child element's content. * text(content, 50, 50); * * describe('The word "Goat" written in black on a gray background.'); * } * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the child element at index 1. * let leopard = myXML.getChild(1); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Get the child element's content. * let content = leopard.getContent(); * * // Display the child element's content. * text(content, 50, 50); * * describe('The word "Leopard" written in black on a gray background.'); * } */ getChild(param) { if (typeof param === 'string') { for (const child of this.DOM.children) { if (child.tagName === param) return new XML(child); } } else { return new XML(this.DOM.children[param]); } } /** * Adds a new child element and returns a reference to it. * * The parameter, `child`, is the p5.XML object to add * as a child element. For example, calling `myXML.addChild(otherXML)` inserts * `otherXML` as a child element of `myXML`. * * @param {p5.XML} child child element to add. * @return {p5.XML} added child element. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Create a new p5.XML object. * let newAnimal = new p5.XML(); * * // Set its properties. * newAnimal.setName('hydrozoa'); * newAnimal.setAttribute('id', 4); * newAnimal.setAttribute('species', 'Physalia physalis'); * newAnimal.setContent('Bluebottle'); * * // Add the child element. * myXML.addChild(newAnimal); * * // Get the first child element that is a hydrozoa. * let blueBottle = myXML.getChild('hydrozoa'); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Get the child element's content. * let content = blueBottle.getContent(); * * // Display the child element's content. * text(content, 50, 50); * * describe('The word "Bluebottle" written in black on a gray background.'); * } */ addChild(node) { if (node instanceof XML) { this.DOM.appendChild(node.DOM); } } /** * Removes the first matching child element. * * The parameter, `name`, is the child element to remove. If a string is * passed, as in `myXML.removeChild('cat')`, then the first child element * with the tag `<cat>` will be removed. If a number is passed, as in * `myXML.removeChild(1)`, then the child element at that index will be * removed. * * @param {String|Integer} name name or index of the child element to remove. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Remove the first mammal element. * myXML.removeChild('mammal'); * * // Get an array of child elements. * let children = myXML.getChildren(); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Iterate over the array. * for (let i = 0; i < children.length; i += 1) { * * // Calculate the y-coordinate. * let y = (i + 1) * 25; * * // Get the child element's content. * let content = children[i].getContent(); * * // Display the child element's content. * text(content, 10, y); * } * * describe( * 'The words "Leopard", "Zebra", and "Turtle" written on separate lines. The text is black on a gray background.' * ); * } * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Remove the element at index 2. * myXML.removeChild(2); * * // Get an array of child elements. * let children = myXML.getChildren(); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Iterate over the array. * for (let i = 0; i < children.length; i += 1) { * * // Calculate the y-coordinate. * let y = (i + 1) * 25; * * // Get the child element's content. * let content = children[i].getContent(); * * // Display the child element's content. * text(content, 10, y); * } * * describe( * 'The words "Goat", "Leopard", and "Turtle" written on separate lines. The text is black on a gray background.' * ); * } */ removeChild(param) { let ind = -1; if (typeof param === 'string') { for (let i = 0; i < this.DOM.children.length; i++) { if (this.DOM.children[i].tagName === param) { ind = i; break; } } } else { ind = param; } if (ind !== -1) { this.DOM.removeChild(this.DOM.children[ind]); } } /** * Returns the number of attributes the element has. * * @return {Integer} number of attributes. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first child element. * let first = myXML.getChild(0); * * // Get the number of attributes. * let numAttributes = first.getAttributeCount(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the number of attributes. * text(numAttributes, 50, 50); * * describe('The number "2" written in black on a gray background.'); * } */ getAttributeCount() { return this.DOM.attributes.length; } /** * Returns an `Array` with the names of the element's attributes. * * Note: Use * myXML.getString() or * myXML.getNum() to return an attribute's value. * * @return {String[]} attribute names. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first child element. * let first = myXML.getChild(0); * * // Get the number of attributes. * let attributes = first.listAttributes(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the element's attributes. * text(attributes, 50, 50); * * describe('The text "id,species" written in black on a gray background.'); * } */ listAttributes() { const arr = []; for (const attribute of this.DOM.attributes) { arr.push(attribute.nodeName); } return arr; } /** * Returns `true` if the element has a given attribute and `false` if not. * * The parameter, `name`, is a string with the name of the attribute being * checked. * * Note: Use * myXML.getString() or * myXML.getNum() to return an attribute's value. * * @param {String} name name of the attribute to be checked. * @return {boolean} whether the element has the attribute. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first mammal child element. * let mammal = myXML.getChild('mammal'); * * // Check whether the element has an * // species attribute. * let hasSpecies = mammal.hasAttribute('species'); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display whether the element has a species attribute. * if (hasSpecies === true) { * text('Species', 50, 50); * } else { * text('No species', 50, 50); * } * * describe('The text "Species" written in black on a gray background.'); * } */ hasAttribute(name) { const obj = {}; for (const attribute of this.DOM.attributes) { obj[attribute.nodeName] = attribute.nodeValue; } return obj[name] ? true : false; } /** * Return an attribute's value as a `Number`. * * The first parameter, `name`, is a string with the name of the attribute * being checked. For example, calling `myXML.getNum('id')` returns the * element's `id` attribute as a number. * * The second parameter, `defaultValue`, is optional. If a number is passed, * as in `myXML.getNum('id', -1)`, it will be returned if the attribute * doesn't exist or can't be converted to a number. * * Note: Use * myXML.getString() or * myXML.getNum() to return an attribute's value. * * @param {String} name name of the attribute to be checked. * @param {Number} [defaultValue] value to return if the attribute doesn't exist. * @return {Number} attribute value as a number. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first reptile child element. * let reptile = myXML.getChild('reptile'); * * // Get the reptile's content. * let content = reptile.getContent(); * * // Get the reptile's ID. * let id = reptile.getNum('id'); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the ID attribute. * text(`${content} is ${id + 1}th`, 5, 50, 90); * * describe(`The text "${content} is ${id + 1}th" written in black on a gray background.`); * } * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first reptile child element. * let reptile = myXML.getChild('reptile'); * * // Get the reptile's content. * let content = reptile.getContent(); * * // Get the reptile's size. * let weight = reptile.getNum('weight', 135); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the ID attribute. * text(`${content} is ${weight}kg`, 5, 50, 90); * * describe( * `The text "${content} is ${weight}kg" written in black on a gray background.` * ); * } */ getNum(name, defaultValue) { const obj = {}; for (const attribute of this.DOM.attributes) { obj[attribute.nodeName] = attribute.nodeValue; } return Number(obj[name]) || defaultValue || 0; } /** * Return an attribute's value as a string. * * The first parameter, `name`, is a string with the name of the attribute * being checked. For example, calling `myXML.getString('color')` returns the * element's `id` attribute as a string. * * The second parameter, `defaultValue`, is optional. If a string is passed, * as in `myXML.getString('color', 'deeppink')`, it will be returned if the * attribute doesn't exist. * * Note: Use * myXML.getString() or * myXML.getNum() to return an attribute's value. * * @param {String} name name of the attribute to be checked. * @param {Number} [defaultValue] value to return if the attribute doesn't exist. * @return {String} attribute value as a string. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first reptile child element. * let reptile = myXML.getChild('reptile'); * * // Get the reptile's content. * let content = reptile.getContent(); * * // Get the reptile's species. * let species = reptile.getString('species'); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the species attribute. * text(`${content}: ${species}`, 5, 50, 90); * * describe(`The text "${content}: ${species}" written in black on a gray background.`); * } * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first reptile child element. * let reptile = myXML.getChild('reptile'); * * // Get the reptile's content. * let content = reptile.getContent(); * * // Get the reptile's color. * let attribute = reptile.getString('color', 'green'); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * fill(attribute); * * // Display the element's content. * text(content, 50, 50); * * describe(`The text "${content}" written in green on a gray background.`); * } */ getString(name, defaultValue) { const obj = {}; for (const attribute of this.DOM.attributes) { obj[attribute.nodeName] = attribute.nodeValue; } return obj[name] ? String(obj[name]) : defaultValue || null; } /** * Sets an attribute to a given value. * * The first parameter, `name`, is a string with the name of the attribute * being set. * * The second parameter, `value`, is the attribute's new value. For example, * calling `myXML.setAttribute('id', 123)` sets the `id` attribute to the * value 123. * * @param {String} name name of the attribute to be set. * @param {Number|String|Boolean} value attribute's new value. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first reptile child element. * let reptile = myXML.getChild('reptile'); * * // Set the reptile's color. * reptile.setAttribute('color', 'green'); * * // Get the reptile's content. * let content = reptile.getContent(); * * // Get the reptile's color. * let attribute = reptile.getString('color'); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the element's content. * text(`${content} is ${attribute}`, 5, 50, 90); * * describe( * `The text "${content} is ${attribute}" written in green on a gray background.` * ); * } */ setAttribute(name, value) { this.DOM.setAttribute(name, value); } /** * Returns the element's content as a `String`. * * The parameter, `defaultValue`, is optional. If a string is passed, as in * `myXML.getContent('???')`, it will be returned if the element has no * content. * * @param {String} [defaultValue] value to return if the element has no * content. * @return {String} element's content as a string. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first reptile child element. * let reptile = myXML.getChild('reptile'); * * // Get the reptile's content. * let content = reptile.getContent(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the element's content. * text(content, 5, 50, 90); * * describe(`The text "${content}" written in green on a gray background.`); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.XML object. * let blankSpace = new p5.XML(); * * // Get the element's content and use a default value. * let content = blankSpace.getContent('Your name'); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the element's content. * text(content, 5, 50, 90); * * describe(`The text "${content}" written in green on a gray background.`); * } */ getContent(defaultValue) { let str; str = this.DOM.textContent; str = str.replace(/\s\s+/g, ','); return str || defaultValue || null; } /** * Sets the element's content. * * An element's content is the text between its tags. For example, the element * `<language>JavaScript</language>` has the content `JavaScript`. * * The parameter, `content`, is a string with the element's new content. * * @method setContent * @param {String} content new content for the element. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get the first reptile child element. * let reptile = myXML.getChild('reptile'); * * // Get the reptile's original content. * let oldContent = reptile.getContent(); * * // Set the reptile's content. * reptile.setContent('Loggerhead'); * * // Get the reptile's new content. * let newContent = reptile.getContent(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(14); * * // Display the element's old and new content. * text(`${oldContent}: ${newContent}`, 5, 50, 90); * * describe( * `The text "${oldContent}: ${newContent}" written in green on a gray background.` * ); * } */ setContent(content) { if (!this.DOM.children.length) { this.DOM.textContent = content; } } /** * Returns the element as a `String`. * * `myXML.serialize()` is useful for sending the element over the network or * saving it to a file. * * @return {String} element as a string. * * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * // Create a p5.PrintWriter object. * // Use the file format .xml. * let myWriter = createWriter('animals', 'xml'); * * // Serialize the XML data to a string. * let data = myXML.serialize(); * * // Write the data to the print stream. * myWriter.write(data); * * // Save the file and close the print stream. * myWriter.close(); * } */ serialize() { const xmlSerializer = new XMLSerializer(); return xmlSerializer.serializeToString(this.DOM); } } function elementsToP5XML(elements) { const arr = []; for (let i = 0; i < elements.length; i++) { arr.push(new XML(elements[i])); } return arr; } function xml(p5, fn){ /** * A class to describe an XML object. * * Each `p5.XML` object provides an easy way to interact with XML data. * Extensible Markup Language * (XML) * is a standard format for sending data between applications. Like HTML, the * XML format is based on tags and attributes, as in * `<time units="s">1234</time>`. * * Note: Use loadXML() to load external XML files. * * @class p5.XML * @example * let myXML; * * async function setup() { * // Load the XML and create a p5.XML object. * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get an array with all mammal tags. * let mammals = myXML.getChildren('mammal'); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Iterate over the mammals array. * for (let i = 0; i < mammals.length; i += 1) { * * // Calculate the y-coordinate. * let y = (i + 1) * 25; * * // Get the mammal's common name. * let name = mammals[i].getContent(); * * // Display the mammal's name. * text(name, 20, y); * } * * describe( * 'The words "Goat", "Leopard", and "Zebra" written on three separate lines. The text is black on a gray background.' * ); * } */ p5.XML = XML; } if(typeof p5 !== 'undefined'){ xml(p5); } /** * @module DOM * @submodule DOM * @for p5.Element */ class File { constructor(file, pInst) { this.file = file; this._pInst = pInst; // Splitting out the file type into two components // This makes determining if image or text etc simpler const typeList = file.type.split('/'); this.type = typeList[0]; this.subtype = typeList[1]; this.name = file.name; this.size = file.size; this.data = undefined; } static _createLoader(theFile, callback) { const reader = new FileReader(); reader.onload = function (e) { const p5file = new File(theFile); if (p5file.file.type === 'application/json') { // Parse JSON and store the result in data p5file.data = JSON.parse(e.target.result); } else if (p5file.file.type === 'text/xml') { // Parse XML, wrap it in p5.XML and store the result in data const parser = new DOMParser(); const xml = parser.parseFromString(e.target.result, 'text/xml'); p5file.data = new XML(xml.documentElement); } else { p5file.data = e.target.result; } callback(p5file); }; return reader; } static _load(f, callback) { // Text or data? // This should likely be improved if (/^text\//.test(f.type) || f.type === 'application/json') { File._createLoader(f, callback).readAsText(f); } else if (!/^(video|audio)\//.test(f.type)) { File._createLoader(f, callback).readAsDataURL(f); } else { const file = new File(f); file.data = URL.createObjectURL(f); callback(file); } } } function file(p5, fn){ /** * A class to describe a file. * * `p5.File` objects are used by * myElement.drop() and * created by * createFileInput. * * @class p5.File * @param {File} file wrapped file. * * @example * // Use the file input to load a * // file and display its info. * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a file input and place it beneath the canvas. * // Call displayInfo() when the file loads. * let input = createFileInput(displayInfo); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.'); * } * * // Display the p5.File's info once it loads. * function displayInfo(file) { * background(200); * * // Display the p5.File's name. * text(file.name, 10, 10, 80, 40); * * // Display the p5.File's type and subtype. * text(`${file.type}/${file.subtype}`, 10, 70); * * // Display the p5.File's size in bytes. * text(file.size, 10, 90); * } * * @example * // Use the file input to select an image to * // load and display. * let img; * * function setup() { * createCanvas(100, 100); * * // Create a file input and place it beneath the canvas. * // Call handleImage() when the file image loads. * let input = createFileInput(handleImage); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user selects an image file to load, it is displayed on the square.'); * } * * function draw() { * background(200); * * // Draw the image if it's ready. * if (img) { * image(img, 0, 0, width, height); * } * } * * // Use the p5.File's data once it loads. * function handleImage(file) { * // Check the p5.File's type. * if (file.type === 'image') { * // Create an image using using the p5.File's data. * img = createImg(file.data, ''); * * // Hide the image element so it doesn't appear twice. * img.hide(); * } else { * img = null; * } * } */ p5.File = File; /** * Underlying * File * object. All `File` properties and methods are accessible. * * @for p5.File * @property {File} file * @example * // Use the file input to load a * // file and display its info. * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a file input and place it beneath the canvas. * // Call displayInfo() when the file loads. * let input = createFileInput(displayInfo); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.'); * } * * // Use the p5.File once it loads. * function displayInfo(file) { * background(200); * * // Display the p5.File's name. * text(file.name, 10, 10, 80, 40); * * // Display the p5.File's type and subtype. * text(`${file.type}/${file.subtype}`, 10, 70); * * // Display the p5.File's size in bytes. * text(file.size, 10, 90); * } */ /** * The file * MIME type * as a string. * * For example, `'image'` and `'text'` are both MIME types. * * @for p5.File * @property {String} type * @example * // Use the file input to load a file and display its info. * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a file input and place it beneath the canvas. * // Call displayType() when the file loads. * let input = createFileInput(displayType); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user loads a file, its type is written in black.'); * } * * // Display the p5.File's type once it loads. * function displayType(file) { * background(200); * * // Display the p5.File's type. * text(`This is file's type is: ${file.type}`, 10, 10, 80, 80); * } */ /** * The file subtype as a string. * * For example, a file with an `'image'` * MIME type * may have a subtype such as ``png`` or ``jpeg``. * * @property {String} subtype * @for p5.File * * @example * // Use the file input to load a * // file and display its info. * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a file input and place it beneath the canvas. * // Call displaySubtype() when the file loads. * let input = createFileInput(displaySubtype); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user loads a file, its subtype is written in black.'); * } * * // Display the p5.File's type once it loads. * function displaySubtype(file) { * background(200); * * // Display the p5.File's subtype. * text(`This is file's subtype is: ${file.subtype}`, 10, 10, 80, 80); * } */ /** * The file name as a string. * * @property {String} name * @for p5.File * * @example * // Use the file input to load a * // file and display its info. * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a file input and place it beneath the canvas. * // Call displayName() when the file loads. * let input = createFileInput(displayName); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user loads a file, its name is written in black.'); * } * * // Display the p5.File's name once it loads. * function displayName(file) { * background(200); * * // Display the p5.File's name. * text(`This is file's name is: ${file.name}`, 10, 10, 80, 80); * } */ /** * The number of bytes in the file. * * @property {Number} size * @for p5.File * * @example * // Use the file input to load a file and display its info. * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a file input and place it beneath the canvas. * // Call displaySize() when the file loads. * let input = createFileInput(displaySize); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user loads a file, its size in bytes is written in black.'); * } * * // Display the p5.File's size in bytes once it loads. * function displaySize(file) { * background(200); * * // Display the p5.File's size. * text(`This is file has ${file.size} bytes.`, 10, 10, 80, 80); * } */ /** * A string containing the file's data. * * Data can be either image data, text contents, or a parsed object in the * case of JSON and p5.XML objects. * * @property {any} data * @for p5.File * * @example * // Use the file input to load a file and display its info. * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a file input and place it beneath the canvas. * // Call displayData() when the file loads. * let input = createFileInput(displayData); * input.position(0, 100); * * describe('A gray square with a file input beneath it. If the user loads a file, its data is written in black.'); * } * * // Display the p5.File's data once it loads. * function displayData(file) { * background(200); * * // Display the p5.File's data, which looks like a random string of characters. * text(file.data, 10, 10, 80, 80); * } */ } if(typeof p5 !== 'undefined'){ file(p5); } /** * @module DOM * @submodule DOM */ class Element { width; height; elt; constructor(elt, pInst) { this.elt = elt; this._pInst = this._pixelsState = pInst; this._events = {}; this.width = this.elt.offsetWidth; this.height = this.elt.offsetHeight; } /** * Removes the element, stops all audio/video streams, and removes all * callback functions. * * @example * let p; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a paragraph element. * p = createP('p5*js'); * p.position(10, 10); * * describe('The text "p5*js" written at the center of a gray square. '); * } * * // Remove the paragraph when the user double-clicks. * function doubleClicked() { * p.remove(); * } */ remove() { // stop all audios/videos and detach all devices like microphone/camera etc // used as input/output for audios/videos. // if (this instanceof p5.MediaElement) { if(this.stop){ this.stop(); const sources = this.elt.srcObject; if (sources !== null) { const tracks = sources.getTracks(); tracks.forEach(track => { track.stop(); }); } } // `this._pInst` is usually the p5 “sketch” object that owns the global // `_elements` array. But when an element lives inside an off-screen // `p5.Graphics` layer, `this._pInst` is that wrapper Graphics object // instead. The wrapper keeps a back–pointer (`_pInst`) to the real // sketch but has no `_elements` array of its own. let sketch = this._pInst; // If `sketch` doesn’t own an `_elements` array it means // we’re still at the graphics-layer “wrapper”. // Jump one level up to the real p5 sketch stored in sketch._pInst. if (sketch && !sketch._elements && sketch._pInst) { sketch = sketch._pInst; // climb one level up } if (sketch && sketch._elements) { // only if the array exists const i = sketch._elements.indexOf(this); if (i !== -1) sketch._elements.splice(i, 1); } // deregister events for (let ev in this._events) { this.elt.removeEventListener(ev, this._events[ev]); } if (this.elt && this.elt.parentNode) { this.elt.parentNode.removeChild(this.elt); } } /** * Attaches the element to a parent element. * * For example, a `<div></div>` element may be used as a box to * hold two pieces of text, a header and a paragraph. The * `<div></div>` is the parent element of both the header and * paragraph. * * The parameter `parent` can have one of three types. `parent` can be a * string with the parent element's ID, as in * `myElement.parent('container')`. It can also be another * p5.Element object, as in * `myElement.parent(myDiv)`. Finally, `parent` can be an `HTMLElement` * object, as in `myElement.parent(anotherElement)`. * * Calling `myElement.parent()` without an argument returns the element's * parent. * * @param {String|p5.Element|Object} parent ID, p5.Element, * or HTMLElement of desired parent element. * @chainable * * @example * function setup() { * background(200); * * // Create a div element. * let div = createDiv(); * * // Place the div in the top-left corner. * div.position(10, 20); * * // Set its width and height. * div.size(80, 60); * * // Set its background color to white * div.style('background-color', 'white'); * * // Align any text to the center. * div.style('text-align', 'center'); * * // Set its ID to "container". * div.id('container'); * * // Create a paragraph element. * let p = createP('p5*js'); * * // Make the div its parent * // using its ID "container". * p.parent('container'); * * describe('The text "p5*js" written in black at the center of a white rectangle. The rectangle is inside a gray square.'); * } * * @example * function setup() { * background(200); * * // Create rectangular div element. * let div = createDiv(); * * // Place the div in the top-left corner. * div.position(10, 20); * * // Set its width and height. * div.size(80, 60); * * // Set its background color and align * // any text to the center. * div.style('background-color', 'white'); * div.style('text-align', 'center'); * * // Create a paragraph element. * let p = createP('p5*js'); * * // Make the div its parent. * p.parent(div); * * describe('The text "p5*js" written in black at the center of a white rectangle. The rectangle is inside a gray square.'); * } * * @example * function setup() { * background(200); * * // Create rectangular div element. * let div = createDiv(); * * // Place the div in the top-left corner. * div.position(10, 20); * * // Set its width and height. * div.size(80, 60); * * // Set its background color and align * // any text to the center. * div.style('background-color', 'white'); * div.style('text-align', 'center'); * * // Create a paragraph element. * let p = createP('p5*js'); * * // Make the div its parent * // using the underlying * // HTMLElement. * p.parent(div.elt); * * describe('The text "p5*js" written in black at the center of a white rectangle. The rectangle is inside a gray square.'); * } */ /** * @return {p5.Element} */ parent(p) { if (typeof p === 'undefined') { return this.elt.parentNode; } if (typeof p === 'string') { if (p[0] === '#') { p = p.substring(1); } p = document.getElementById(p); } else if (p instanceof Element) { p = p.elt; } p.appendChild(this.elt); return this; } /** * Attaches the element as a child of another element. * * `myElement.child()` accepts either a string ID, DOM node, or * p5.Element. For example, * `myElement.child(otherElement)`. If no argument is provided, an array of * children DOM nodes is returned. * * @returns {Node[]} an array of child nodes. * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the div elements. * let div0 = createDiv('Parent'); * let div1 = createDiv('Child'); * * // Make div1 the child of div0 * // using the p5.Element. * div0.child(div1); * * describe('A gray square with the words "Parent" and "Child" written beneath it.'); * } * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the div elements. * let div0 = createDiv('Parent'); * let div1 = createDiv('Child'); * * // Give div1 an ID. * div1.id('apples'); * * // Make div1 the child of div0 * // using its ID. * div0.child('apples'); * * describe('A gray square with the words "Parent" and "Child" written beneath it.'); * } * * @example * // META:norender * // This example assumes there is a div already on the page * // with id "myChildDiv". * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the div elements. * let div0 = createDiv('Parent'); * * // Select the child element by its ID. * let elt = document.getElementById('myChildDiv'); * * // Make div1 the child of div0 * // using its HTMLElement object. * div0.child(elt); * * describe('A gray square with the words "Parent" and "Child" written beneath it.'); * } */ /** * @param {String|p5.Element} [child] the ID, DOM node, or p5.Element * to add to the current element * @chainable */ child(childNode) { if (typeof childNode === 'undefined') { return this.elt.childNodes; } if (typeof childNode === 'string') { if (childNode[0] === '#') { childNode = childNode.substring(1); } childNode = document.getElementById(childNode); } else if (childNode instanceof Element) { childNode = childNode.elt; } if (childNode instanceof HTMLElement) { this.elt.appendChild(childNode); } return this; } /** * Sets the inner HTML of the element, replacing any existing HTML. * * The second parameter, `append`, is optional. If `true` is passed, as in * `myElement.html('hi', true)`, the HTML is appended instead of replacing * existing HTML. * * If no arguments are passed, as in `myElement.html()`, the element's inner * HTML is returned. * * @for p5.Element * @returns {String} the inner HTML of the element * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * // Create the div element and set its size. * let div = createDiv(''); * div.size(100, 100); * * // Set the inner HTML to "hi". * div.html('hi'); * * describe('A gray square with the word "hi" written beneath it.'); * } * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the div element and set its size. * let div = createDiv('Hello '); * div.size(100, 100); * * // Append "World" to the div's HTML. * div.html('World', true); * * describe('A gray square with the text "Hello World" written beneath it.'); * } * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the div element. * let div = createDiv('Hello'); * * // Prints "Hello" to the console. * print(div.html()); * * describe('A gray square with the word "Hello!" written beneath it.'); * } */ /** * @param {String} [html] the HTML to be placed inside the element * @param {Boolean} [append] whether to append HTML to existing * @chainable */ html(...args) { if (args.length === 0) { return this.elt.innerHTML; } else if (args[1]) { this.elt.insertAdjacentHTML('beforeend', args[0]); return this; } else { this.elt.innerHTML = args[0]; return this; } } /** * Sets the element's ID using a given string. * * Calling `myElement.id()` without an argument returns its ID as a string. * * @param {String} id ID of the element. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Set the canvas' ID * // to "mycanvas". * cnv.id('mycanvas'); * * // Get the canvas' ID. * let id = cnv.id(); * text(id, 24, 54); * * describe('The text "mycanvas" written in black on a gray background.'); * } */ /** * @return {String} ID of the element. */ id(id) { if (typeof id === 'undefined') { return this.elt.id; } this.elt.id = id; this.width = this.elt.offsetWidth; this.height = this.elt.offsetHeight; return this; } /** * Adds a * class attribute * to the element using a given string. * * Calling `myElement.class()` without an argument returns a string with its current classes. * * @param {String} class class to add. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Add the class "small" to the * // canvas element. * cnv.class('small'); * * // Get the canvas element's class * // and display it. * let c = cnv.class(); * text(c, 35, 54); * * describe('The word "small" written in black on a gray canvas.'); * * } */ /** * @return {String} element's classes, if any. */ class(c) { if (typeof c === 'undefined') { return this.elt.className; } this.elt.className = c; return this; } /** * * Adds a class to the element. * * @for p5.Element * @param {String} class name of class to add. * @chainable * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a div element. * let div = createDiv('div'); * * // Add a class to the div. * div.addClass('myClass'); * * describe('A gray square.'); * } */ addClass(c) { if (this.elt.className) { if (!this.hasClass(c)) { this.elt.className = this.elt.className + ' ' + c; } } else { this.elt.className = c; } return this; } /** * Removes a class from the element. * * @param {String} class name of class to remove. * @chainable * * @example * // META:norender * // In this example, a class is set when the div is created * // and removed when mouse is pressed. This could link up * // with a CSS style rule to toggle style properties. * * let div; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a div element. * div = createDiv('div'); * * // Add a class to the div. * div.addClass('myClass'); * * describe('A gray square.'); * } * * // Remove 'myClass' from the div when the user presses the mouse. * function mousePressed() { * div.removeClass('myClass'); * } */ removeClass(c) { // Note: Removing a class that does not exist does NOT throw an error in classList.remove method this.elt.classList.remove(c); return this; } /** * Checks if a class is already applied to element. * * @returns {boolean} a boolean value if element has specified class. * @param c {String} name of class to check. * * @example * // META:norender * let div; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a div element. * div = createDiv('div'); * * // Add the class 'show' to the div. * div.addClass('show'); * * describe('A gray square.'); * } * * // Toggle the class 'show' when the mouse is pressed. * function mousePressed() { * if (div.hasClass('show')) { * div.addClass('show'); * } else { * div.removeClass('show'); * } * } */ hasClass(c) { return this.elt.classList.contains(c); } /** * Toggles whether a class is applied to the element. * * @param c {String} class name to toggle. * @chainable * * @example * // META:norender * let div; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a div element. * div = createDiv('div'); * * // Add the 'show' class to the div. * div.addClass('show'); * * describe('A gray square.'); * } * * // Toggle the 'show' class when the mouse is pressed. * function mousePressed() { * div.toggleClass('show'); * } */ toggleClass(c) { // classList also has a toggle() method, but we cannot use that yet as support is unclear. // See https://github.com/processing/p5.js/issues/3631 // this.elt.classList.toggle(c); if (this.elt.classList.contains(c)) { this.elt.classList.remove(c); } else { this.elt.classList.add(c); } return this; } /** * Centers the element either vertically, horizontally, or both. * * `center()` will center the element relative to its parent or according to * the page's body if the element has no parent. * * If no argument is passed, as in `myElement.center()` the element is aligned * both vertically and horizontally. * * @param {String} [align] passing 'vertical', 'horizontal' aligns element accordingly * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the div element and style it. * let div = createDiv(''); * div.size(10, 10); * div.style('background-color', 'orange'); * * // Center the div relative to the page's body. * div.center(); * * describe('A gray square and an orange rectangle. The rectangle is at the center of the page.'); * } */ center(align) { const style = this.elt.style.display; const hidden = this.elt.style.display === 'none'; const parentHidden = this.parent().style.display === 'none'; const pos = { x: this.elt.offsetLeft, y: this.elt.offsetTop }; if (hidden) this.show(); if (parentHidden) this.parent().show(); this.elt.style.display = 'block'; this.position(0, 0); const wOffset = Math.abs(this.parent().offsetWidth - this.elt.offsetWidth); const hOffset = Math.abs( this.parent().offsetHeight - this.elt.offsetHeight ); if (align === 'both' || align === undefined) { this.position( wOffset / 2 + this.parent().offsetLeft, hOffset / 2 + this.parent().offsetTop ); } else if (align === 'horizontal') { this.position(wOffset / 2 + this.parent().offsetLeft, pos.y); } else if (align === 'vertical') { this.position(pos.x, hOffset / 2 + this.parent().offsetTop); } this.style('display', style); if (hidden) this.hide(); if (parentHidden) this.parent().hide(); return this; } /** * Sets the element's position. * * The first two parameters, `x` and `y`, set the element's position relative * to the top-left corner of the web page. * * The third parameter, `positionType`, is optional. It sets the element's * positioning scheme. * `positionType` is a string that can be either `'static'`, `'fixed'`, * `'relative'`, `'sticky'`, `'initial'`, or `'inherit'`. * * If no arguments passed, as in `myElement.position()`, the method returns * the element's position in an object, as in `{ x: 0, y: 0 }`. * * @returns {Object} object of form `{ x: 0, y: 0 }` containing the element's position. * * @example * // META:norender * function setup() { * let cnv = createCanvas(100, 100); * * background(200); * * // Positions the canvas 50px to the right and 100px * // below the top-left corner of the window. * cnv.position(50, 100); * * describe('A gray square that is 50 pixels to the right and 100 pixels down from the top-left corner of the web page.'); * } * * @example * // META:norender * function setup() { * let cnv = createCanvas(100, 100); * * background(200); * * // Positions the canvas at the top-left corner * // of the window with a 'fixed' position type. * cnv.position(0, 0, 'fixed'); * * describe('A gray square in the top-left corner of the web page.'); * } */ /** * @param {Number} [x] x-position relative to top-left of window (optional) * @param {Number} [y] y-position relative to top-left of window (optional) * @param {String} [positionType] it can be static, fixed, relative, sticky, initial or inherit (optional) * @chainable */ position(...args) { if (args.length === 0) { return { x: this.elt.offsetLeft, y: this.elt.offsetTop }; } else { let positionType = 'absolute'; if ( args[2] === 'static' || args[2] === 'fixed' || args[2] === 'relative' || args[2] === 'sticky' || args[2] === 'initial' || args[2] === 'inherit' ) { positionType = args[2]; } this.elt.style.position = positionType; this.elt.style.left = args[0] + 'px'; this.elt.style.top = args[1] + 'px'; this.x = args[0]; this.y = args[1]; return this; } } /** * Shows the current element. * * @chainable * * @example * let p; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a paragraph element and hide it. * p = createP('p5*js'); * p.position(10, 10); * p.hide(); * * describe('A gray square. The text "p5*js" appears when the user double-clicks the square.'); * } * * // Show the paragraph when the user double-clicks. * function doubleClicked() { * p.show(); * } */ show() { this.elt.style.display = 'block'; return this; } /** * Hides the current element. * * @chainable * * @example * let p; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a paragraph element. * p = createP('p5*js'); * p.position(10, 10); * * describe('The text "p5*js" at the center of a gray square. The text disappears when the user double-clicks the square.'); * } * * // Hide the paragraph when the user double-clicks. * function doubleClicked() { * p.hide(); * } */ hide() { this.elt.style.display = 'none'; return this; } /** * Sets the element's width and height. * * Calling `myElement.size()` without an argument returns the element's size * as an object with the properties `width` and `height`. For example, * `{ width: 20, height: 10 }`. * * The first parameter, `width`, is optional. It's a number used to set the * element's width. Calling `myElement.size(10)` * * The second parameter, 'height`, is also optional. It's a * number used to set the element's height. For example, calling * `myElement.size(20, 10)` sets the element's width to 20 pixels and height * to 10 pixels. * * The constant `AUTO` can be used to adjust one dimension at a time while * maintaining the aspect ratio, which is `width / height`. For example, * consider an element that's 200 pixels wide and 100 pixels tall. Calling * `myElement.size(20, AUTO)` sets the width to 20 pixels and height to 10 * pixels. * * Note: In the case of elements that need to load data, such as images, wait * to call `myElement.size()` until after the data loads. * * @return {Object} width and height of the element in an object. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a pink div element and place it at the top-left corner. * let div = createDiv(); * div.position(10, 10); * div.style('background-color', 'deeppink'); * * // Set the div's width to 80 pixels and height to 20 pixels. * div.size(80, 20); * * describe('A gray square with a pink rectangle near its top.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a pink div element and place it at the top-left corner. * let div = createDiv(); * div.position(10, 10); * div.style('background-color', 'deeppink'); * * // Set the div's width to 80 pixels and height to 40 pixels. * div.size(80, 40); * * // Get the div's size as an object. * let s = div.size(); * * // Display the div's dimensions. * div.html(`${s.width} x ${s.height}`); * * describe('A gray square with a pink rectangle near its top. The text "80 x 40" is written within the rectangle.'); * } * * @example * let img1; * let img2; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Load an image of an astronaut on the moon * // and place it at the top-left of the canvas. * img1 = createImg( * 'assets/moonwalk.jpg', * 'An astronaut walking on the moon', * '' * ); * img1.position(0, 0); * * // Load an image of an astronaut on the moon * // and place it at the top-left of the canvas. * // Resize the image once it's loaded. * img2 = createImg( * 'assets/moonwalk.jpg', * 'An astronaut walking on the moon', * '', * resizeImage * ); * img2.position(0, 0); * * describe('A gray square two copies of a space image at the top-left. The copy in front is smaller.'); * } * * // Resize img2 and keep its aspect ratio. * function resizeImage() { * img2.size(50, AUTO); * } */ /** * @param {(Number|AUTO)} [w] width of the element, either AUTO, or a number. * @param {(Number|AUTO)} [h] height of the element, either AUTO, or a number. * @chainable */ size(w, h) { if (arguments.length === 0) { return { width: this.elt.offsetWidth, height: this.elt.offsetHeight }; } else { let aW = w; let aH = h; const AUTO$1 = AUTO; if (aW !== AUTO$1 || aH !== AUTO$1) { if (aW === AUTO$1) { aW = h * this.width / this.height; } else if (aH === AUTO$1) { aH = w * this.height / this.width; } // set diff for cnv vs normal div if (this.elt instanceof HTMLCanvasElement) { const j = {}; const k = this.elt.getContext('2d'); let prop; for (prop in k) { j[prop] = k[prop]; } this.elt.setAttribute('width', aW * this._pInst._pixelDensity); this.elt.setAttribute('height', aH * this._pInst._pixelDensity); this.elt.style.width = aW + 'px'; this.elt.style.height = aH + 'px'; this._pInst.scale( this._pInst._pixelDensity, this._pInst._pixelDensity ); for (prop in j) { this.elt.getContext('2d')[prop] = j[prop]; } } else { this.elt.style.width = aW + 'px'; this.elt.style.height = aH + 'px'; this.elt.width = aW; this.elt.height = aH; } this.width = aW; this.height = aH; if (this._pInst && this._pInst._curElement) { // main canvas associated with p5 instance if (this._pInst._curElement.elt === this.elt) { this._pInst.width = aW; this._pInst.height = aH; } } } return this; } } /** * Applies a style to the element by adding a * CSS declaration. * * The first parameter, `property`, is a string. If the name of a style * property is passed, as in `myElement.style('color')`, the method returns * the current value as a string or `null` if it hasn't been set. If a * `property:style` string is passed, as in * `myElement.style('color:deeppink')`, the method sets the style `property` * to `value`. * * The second parameter, `value`, is optional. It sets the property's value. * `value` can be a string, as in * `myElement.style('color', 'deeppink')`, or a * p5.Color object, as in * `myElement.style('color', myColor)`. * * @param {String} property style property to set. * @returns {String} value of the property. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a paragraph element and set its font color to "deeppink". * let p = createP('p5*js'); * p.position(25, 20); * p.style('color', 'deeppink'); * * describe('The text p5*js written in pink on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let c = color('deeppink'); * * // Create a paragraph element and set its font color using a p5.Color object. * let p = createP('p5*js'); * p.position(25, 20); * p.style('color', c); * * describe('The text p5*js written in pink on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a paragraph element and set its font color to "deeppink" * // using property:value syntax. * let p = createP('p5*js'); * p.position(25, 20); * p.style('color:deeppink'); * * describe('The text p5*js written in pink on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create an empty paragraph element and set its font color to "deeppink". * let p = createP(); * p.position(5, 5); * p.style('color', 'deeppink'); * * // Get the element's color as an RGB color string. * let c = p.style('color'); * * // Set the element's inner HTML using the RGB color string. * p.html(c); * * describe('The text "rgb(255, 20, 147)" written in pink on a gray background.'); * } */ /** * @param {String} property * @param {String|p5.Color} value value to assign to the property. * @return {String} value of the property. * @chainable */ style(prop, val) { const self = this; if (val instanceof Color) { val = val.toString(); } if (typeof val === 'undefined') { if (prop.indexOf(':') === -1) { // no value set, so assume requesting a value let styles = window.getComputedStyle(self.elt); let style = styles.getPropertyValue(prop); return style; } else { // value set using `:` in a single line string const attrs = prop.split(';'); for (let i = 0; i < attrs.length; i++) { const parts = attrs[i].split(':'); if (parts[0] && parts[1]) { this.elt.style[parts[0].trim()] = parts[1].trim(); } } } } else { // input provided as key,val pair this.elt.style[prop] = val; if ( prop === 'width' || prop === 'height' || prop === 'left' || prop === 'top' ) { let styles = window.getComputedStyle(self.elt); let styleVal = styles.getPropertyValue(prop); let numVal = styleVal.replace(/[^\d.]/g, ''); this[prop] = Math.round(parseFloat(numVal, 10)); } } return this; } /* Helper method called by p5.Element.style() */ _translate(...args) { this.elt.style.position = 'absolute'; // save out initial non-translate transform styling let transform = ''; if (this.elt.style.transform) { transform = this.elt.style.transform.replace(/translate3d\(.*\)/g, ''); transform = transform.replace(/translate[X-Z]?\(.*\)/g, ''); } if (args.length === 2) { this.elt.style.transform = 'translate(' + args[0] + 'px, ' + args[1] + 'px)'; } else if (args.length > 2) { this.elt.style.transform = 'translate3d(' + args[0] + 'px,' + args[1] + 'px,' + args[2] + 'px)'; if (args.length === 3) { this.elt.parentElement.style.perspective = '1000px'; } else { this.elt.parentElement.style.perspective = args[3] + 'px'; } } // add any extra transform styling back on end this.elt.style.transform += transform; return this; } /* Helper method called by p5.Element.style() */ _rotate(...args) { // save out initial non-rotate transform styling let transform = ''; if (this.elt.style.transform) { transform = this.elt.style.transform.replace(/rotate3d\(.*\)/g, ''); transform = transform.replace(/rotate[X-Z]?\(.*\)/g, ''); } if (args.length === 1) { this.elt.style.transform = 'rotate(' + args[0] + 'deg)'; } else if (args.length === 2) { this.elt.style.transform = 'rotate(' + args[0] + 'deg, ' + args[1] + 'deg)'; } else if (args.length === 3) { this.elt.style.transform = 'rotateX(' + args[0] + 'deg)'; this.elt.style.transform += 'rotateY(' + args[1] + 'deg)'; this.elt.style.transform += 'rotateZ(' + args[2] + 'deg)'; } // add remaining transform back on this.elt.style.transform += transform; return this; } /** * Adds an * attribute * to the element. * * This method is useful for advanced tasks. Most commonly-used attributes, * such as `id`, can be set with their dedicated methods. For example, * `nextButton.id('next')` sets an element's `id` attribute. Calling * `nextButton.attribute('id', 'next')` has the same effect. * * The first parameter, `attr`, is the attribute's name as a string. Calling * `myElement.attribute('align')` returns the attribute's current value as a * string or `null` if it hasn't been set. * * The second parameter, `value`, is optional. It's a string used to set the * attribute's value. For example, calling * `myElement.attribute('align', 'center')` sets the element's horizontal * alignment to `center`. * * @return {String} value of the attribute. * * @example * function setup() { * createCanvas(100, 100); * * // Create a container div element and place it at the top-left corner. * let container = createDiv(); * container.position(0, 0); * * // Create a paragraph element and place it within the container. * // Set its horizontal alignment to "left". * let p1 = createP('hi'); * p1.parent(container); * p1.attribute('align', 'left'); * * // Create a paragraph element and place it within the container. * // Set its horizontal alignment to "center". * let p2 = createP('hi'); * p2.parent(container); * p2.attribute('align', 'center'); * * // Create a paragraph element and place it within the container. * // Set its horizontal alignment to "right". * let p3 = createP('hi'); * p3.parent(container); * p3.attribute('align', 'right'); * * describe('A gray square with the text "hi" written on three separate lines, each placed further to the right.'); * } */ /** * @param {String} attr attribute to set. * @param {String} value value to assign to the attribute. * @chainable */ attribute(attr, value) { //handling for checkboxes and radios to ensure options get //attributes not divs if ( this.elt.firstChild != null && (this.elt.firstChild.type === 'checkbox' || this.elt.firstChild.type === 'radio') ) { if (typeof value === 'undefined') { return this.elt.firstChild.getAttribute(attr); } else { for (let i = 0; i < this.elt.childNodes.length; i++) { this.elt.childNodes[i].setAttribute(attr, value); } } } else if (typeof value === 'undefined') { return this.elt.getAttribute(attr); } else { this.elt.setAttribute(attr, value); return this; } } /** * Removes an attribute from the element. * * The parameter `attr` is the attribute's name as a string. For example, * calling `myElement.removeAttribute('align')` removes its `align` * attribute if it's been set. * * @param {String} attr attribute to remove. * @chainable * * @example * let p; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a paragraph element and place it in the center of the canvas. * // Set its "align" attribute to "center". * p = createP('hi'); * p.position(0, 20); * p.attribute('align', 'center'); * * describe('The text "hi" written in black at the center of a gray square. The text moves to the left edge when double-clicked.'); * } * * // Remove the 'align' attribute when the user double-clicks the paragraph. * function doubleClicked() { * p.removeAttribute('align'); * } */ removeAttribute(attr) { if ( this.elt.firstChild != null && (this.elt.firstChild.type === 'checkbox' || this.elt.firstChild.type === 'radio') ) { for (let i = 0; i < this.elt.childNodes.length; i++) { this.elt.childNodes[i].removeAttribute(attr); } } this.elt.removeAttribute(attr); return this; } /** * Returns or sets the element's value. * * Calling `myElement.value()` returns the element's current value. * * The parameter, `value`, is an optional number or string. If provided, * as in `myElement.value(123)`, it's used to set the element's value. * * @return {String|Number} value of the element. * * @example * let input; * * function setup() { * createCanvas(100, 100); * * // Create a text input and place it beneath the canvas. * // Set its default value to "hello". * input = createInput('hello'); * input.position(0, 100); * * describe('The text from an input box is displayed on a gray square.'); * } * * function draw() { * background(200); * * // Use the input's value to display a message. * let msg = input.value(); * text(msg, 0, 55); * } * * @example * let input; * * function setup() { * createCanvas(100, 100); * * // Create a text input and place it beneath the canvas. * // Set its default value to "hello". * input = createInput('hello'); * input.position(0, 100); * * describe('The text from an input box is displayed on a gray square. The text resets to "hello" when the user double-clicks the square.'); * } * * function draw() { * background(200); * * // Use the input's value to display a message. * let msg = input.value(); * text(msg, 0, 55); * } * * // Reset the input's value. * function doubleClicked() { * input.value('hello'); * } */ /** * @param {String|Number} value * @chainable */ value(...args) { if (args.length > 0) { this.elt.value = args[0]; return this; } else { if (this.elt.type === 'range') { return parseFloat(this.elt.value); } else return this.elt.value; } } /** * Calls a function when the mouse is pressed over the element. * * Calling `myElement.mousePressed(false)` disables the function. * * Note: Some mobile browsers may also trigger this event when the element * receives a quick tap. * * @param {Function|Boolean} fxn function to call when the mouse is * pressed over the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when the canvas * // is pressed. * cnv.mousePressed(randomColor); * * describe('A gray square changes color when the mouse is pressed.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ mousePressed(fxn) { // Prepend the mouse property setters to the event-listener. // This is required so that mouseButton is set correctly prior to calling the callback (fxn). // For details, see https://github.com/processing/p5.js/issues/3087. const eventPrependedFxn = function (event) { this._pInst.mouseIsPressed = true; this._pInst._activePointers.set(event.pointerId, event); this._pInst._setMouseButton(event); this._pInst._updatePointerCoords(event); // Pass along the return-value of the callback: return fxn.call(this, event); }; // Pass along the event-prepended form of the callback. Element._adjustListener('pointerdown', eventPrependedFxn, this); return this; } /** * Calls a function when the mouse is pressed twice over the element. * * Calling `myElement.doubleClicked(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the mouse is * double clicked over the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when the * // canvas is double-clicked. * cnv.doubleClicked(randomColor); * * describe('A gray square changes color when the user double-clicks the canvas.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ doubleClicked(fxn) { Element._adjustListener('dblclick', fxn, this); return this; } /** * Calls a function when the mouse wheel scrolls over the element. * * The callback function, `fxn`, is passed an `event` object. `event` has * two numeric properties, `deltaY` and `deltaX`. `event.deltaY` is * negative if the mouse wheel rotates away from the user. It's positive if * the mouse wheel rotates toward the user. `event.deltaX` is positive if * the mouse wheel moves to the right. It's negative if the mouse wheel moves * to the left. * * Calling `myElement.mouseWheel(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the mouse wheel is * scrolled over the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when the * // mouse wheel moves. * cnv.mouseWheel(randomColor); * * describe('A gray square changes color when the user scrolls the mouse wheel over the canvas.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call changeBackground() when the * // mouse wheel moves. * cnv.mouseWheel(changeBackground); * * describe('A gray square. When the mouse wheel scrolls over the square, it changes color and displays shapes.'); * } * * function changeBackground(event) { * // Change the background color * // based on deltaY. * if (event.deltaY > 0) { * background('deeppink'); * } else if (event.deltaY < 0) { * background('cornflowerblue'); * } else { * background(200); * } * * // Draw a shape based on deltaX. * if (event.deltaX > 0) { * circle(50, 50, 20); * } else if (event.deltaX < 0) { * square(40, 40, 20); * } * } */ mouseWheel(fxn) { Element._adjustListener('wheel', fxn, this); return this; } /** * Calls a function when the mouse is released over the element. * * Calling `myElement.mouseReleased(false)` disables the function. * * Note: Some mobile browsers may also trigger this event when the element * receives a quick tap. * * @param {Function|Boolean} fxn function to call when the mouse is * pressed over the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when a * // mouse press ends. * cnv.mouseReleased(randomColor); * * describe('A gray square changes color when the user releases a mouse press.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ mouseReleased(fxn) { Element._adjustListener('pointerup', fxn, this); return this; } /** * Calls a function when the mouse is pressed and released over the element. * * Calling `myElement.mouseReleased(false)` disables the function. * * Note: Some mobile browsers may also trigger this event when the element * receives a quick tap. * * @param {Function|Boolean} fxn function to call when the mouse is * pressed and released over the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when a * // mouse press ends. * cnv.mouseClicked(randomColor); * * describe('A gray square changes color when the user releases a mouse press.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ mouseClicked(fxn) { Element._adjustListener('click', fxn, this); return this; } /** * Calls a function when the mouse moves over the element. * * Calling `myElement.mouseMoved(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the mouse * moves over the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when the * // mouse moves. * cnv.mouseMoved(randomColor); * * describe('A gray square changes color when the mouse moves over the canvas.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ mouseMoved(fxn) { Element._adjustListener('pointermove', fxn, this); return this; } /** * Calls a function when the mouse moves onto the element. * * Calling `myElement.mouseOver(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the mouse * moves onto the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when the * // mouse moves onto the canvas. * cnv.mouseOver(randomColor); * * describe('A gray square changes color when the mouse moves onto the canvas.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ mouseOver(fxn) { Element._adjustListener('pointerover', fxn, this); return this; } /** * Calls a function when the mouse moves off the element. * * Calling `myElement.mouseOut(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the mouse * moves off the element. * `false` disables the function. * @chainable * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call randomColor() when the * // mouse moves off the canvas. * cnv.mouseOut(randomColor); * * describe('A gray square changes color when the mouse moves off the canvas.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ mouseOut(fxn) { Element._adjustListener('pointerout', fxn, this); return this; } /** * Calls a function when a file is dragged over the element. * * Calling `myElement.dragOver(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the file is * dragged over the element. * `false` disables the function. * @chainable * * @example * // Drag a file over the canvas to test. * * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call helloFile() when a * // file is dragged over * // the canvas. * cnv.dragOver(helloFile); * * describe('A gray square. The text "hello, file" appears when a file is dragged over the square.'); * } * * function helloFile() { * text('hello, file', 50, 50); * } */ dragOver(fxn) { Element._adjustListener('dragover', fxn, this); return this; } /** * Calls a function when a file is dragged off the element. * * Calling `myElement.dragLeave(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the file is * dragged off the element. * `false` disables the function. * @chainable * @example * // Drag a file over, then off * // the canvas to test. * * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Call byeFile() when a * // file is dragged over, * // then off the canvas. * cnv.dragLeave(byeFile); * * describe('A gray square. The text "bye, file" appears when a file is dragged over, then off the square.'); * } * * function byeFile() { * text('bye, file', 50, 50); * } */ dragLeave(fxn) { Element._adjustListener('dragleave', fxn, this); return this; } /** * Calls a function when the element changes. * * Calling `myElement.changed(false)` disables the function. * * @param {Function|Boolean} fxn function to call when the element changes. * `false` disables the function. * @chainable * * @example * let dropdown; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a dropdown menu and add a few color options. * dropdown = createSelect(); * dropdown.position(0, 0); * dropdown.option('red'); * dropdown.option('green'); * dropdown.option('blue'); * * // Call paintBackground() when the color option changes. * dropdown.changed(paintBackground); * * describe('A gray square with a dropdown menu at the top. The square changes color when an option is selected.'); * } * * // Paint the background with the selected color. * function paintBackground() { * let c = dropdown.value(); * background(c); * } * * @example * let checkbox; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a checkbox and place it beneath the canvas. * checkbox = createCheckbox(' circle'); * checkbox.position(0, 100); * * // Call repaint() when the checkbox changes. * checkbox.changed(repaint); * * describe('A gray square with a checkbox underneath it that says "circle". A white circle appears when the box is checked and disappears otherwise.'); * } * * // Paint the background gray and determine whether to draw a circle. * function repaint() { * background(200); * if (checkbox.checked() === true) { * circle(50, 50, 30); * } * } */ changed(fxn) { Element._adjustListener('change', fxn, this); return this; } /** * Calls a function when the element receives input. * * `myElement.input()` is often used to with text inputs and sliders. Calling * `myElement.input(false)` disables the function. * * @param {Function|Boolean} fxn function to call when input is detected within * the element. * `false` disables the function. * @chainable * * @example * let slider; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a slider and place it beneath the canvas. * slider = createSlider(0, 255, 200); * slider.position(0, 100); * * // Call repaint() when the slider changes. * slider.input(repaint); * * describe('A gray square with a range slider underneath it. The background changes shades of gray when the slider is moved.'); * } * * // Paint the background using slider's value. * function repaint() { * let g = slider.value(); * background(g); * } * * @example * let input; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create an input and place it beneath the canvas. * input = createInput(''); * input.position(0, 100); * * // Call repaint() when input is detected. * input.input(repaint); * * describe('A gray square with a text input bar beneath it. Any text written in the input appears in the middle of the square.'); * } * * // Paint the background gray and display the input's value. * function repaint() { * background(200); * let msg = input.value(); * text(msg, 5, 50); * } */ input(fxn) { Element._adjustListener('input', fxn, this); return this; } /** * Calls a function when the user drops a file on the element. * * The first parameter, `callback`, is a function to call once the file loads. * The callback function should have one parameter, `file`, that's a * p5.File object. If the user drops multiple files on * the element, `callback`, is called once for each file. * * The second parameter, `fxn`, is a function to call when the browser detects * one or more dropped files. The callback function should have one * parameter, `event`, that's a * DragEvent. * * @param {Function} callback called when a file loads. Called once for each file dropped. * @param {Function} [fxn] called once when any files are dropped. * @chainable * * @example * // Drop an image on the canvas to view * // this example. * let img; * * function setup() { * let c = createCanvas(100, 100); * * background(200); * * // Call handleFile() when a file that's dropped on the canvas has loaded. * c.drop(handleFile); * * describe('A gray square. When the user drops an image on the square, it is displayed.'); * } * * // Remove the existing image and display the new one. * function handleFile(file) { * // Remove the current image, if any. * if (img) { * img.remove(); * } * * // Create an element with the * // dropped file. * img = createImg(file.data, ''); * img.hide(); * * // Draw the image. * image(img, 0, 0, width, height); * } * * @example * // Drop an image on the canvas to view * // this example. * let img; * let msg; * * function setup() { * let c = createCanvas(100, 100); * * background(200); * * // Call functions when the user drops a file on the canvas * // and when the file loads. * c.drop(handleFile, handleDrop); * * describe('A gray square. When the user drops an image on the square, it is displayed. The id attribute of canvas element is also displayed.'); * } * * // Display the image when it loads. * function handleFile(file) { * // Remove the current image, if any. * if (img) { * img.remove(); * } * * // Create an img element with the dropped file. * img = createImg(file.data, ''); * img.hide(); * * // Draw the image. * image(img, 0, 0, width, height); * } * * // Display the file's name when it loads. * function handleDrop(event) { * // Remove current paragraph, if any. * if (msg) { * msg.remove(); * } * * // Use event to get the drop target's id. * let id = event.target.id; * * // Write the canvas' id beneath it. * msg = createP(id); * msg.position(0, 100); * * // Set the font color randomly for each drop. * let c = random(['red', 'green', 'blue']); * msg.style('color', c); * msg.style('font-size', '12px'); * } */ drop(callback, fxn) { // Is the file stuff supported? if (window.File && window.FileReader && window.FileList && window.Blob) { if (!this._dragDisabled) { this._dragDisabled = true; const preventDefault = function (evt) { evt.preventDefault(); }; // If you want to be able to drop you've got to turn off // a lot of default behavior. // avoid `attachListener` here, since it overrides other handlers. this.elt.addEventListener('dragover', preventDefault); // If this is a drag area we need to turn off the default behavior this.elt.addEventListener('dragleave', preventDefault); } // Deal with the files Element._attachListener( 'drop', function (evt) { evt.preventDefault(); // Call the second argument as a callback that receives the raw drop event if (typeof fxn === 'function') { fxn.call(this, evt); } // A FileList const files = evt.dataTransfer.files; // Load each one and trigger the callback for (const f of files) { File._load(f, callback); } }, this ); } else { console.log('The File APIs are not fully supported in this browser.'); } return this; } /** * Makes the element draggable. * * The parameter, `elmnt`, is optional. If another * p5.Element object is passed, as in * `myElement.draggable(otherElement)`, the other element will become draggable. * * @param {p5.Element} [elmnt] another p5.Element. * @chainable * * @example * let stickyNote; * let textInput; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a div element and style it. * stickyNote = createDiv('Note'); * stickyNote.position(5, 5); * stickyNote.size(80, 20); * stickyNote.style('font-size', '16px'); * stickyNote.style('font-family', 'Comic Sans MS'); * stickyNote.style('background', 'orchid'); * stickyNote.style('padding', '5px'); * * // Make the note draggable. * stickyNote.draggable(); * * // Create a panel div and style it. * let panel = createDiv(''); * panel.position(5, 40); * panel.size(80, 50); * panel.style('background', 'orchid'); * panel.style('font-size', '16px'); * panel.style('padding', '5px'); * panel.style('text-align', 'center'); * * // Make the panel draggable. * panel.draggable(); * * // Create a text input and style it. * textInput = createInput('Note'); * textInput.size(70); * * // Add the input to the panel. * textInput.parent(panel); * * // Call handleInput() when text is input. * textInput.input(handleInput); * * describe( * 'A gray square with two purple rectangles that move when dragged. The top rectangle displays the text that is typed into the bottom rectangle.' * ); * } * * // Update stickyNote's HTML when text is input. * function handleInput() { * stickyNote.html(textInput.value()); * } */ draggable(elmMove) { let isTouch = 'ontouchstart' in window; let x = 0, y = 0, px = 0, py = 0, elmDrag, dragMouseDownEvt = isTouch ? 'touchstart' : 'mousedown', closeDragElementEvt = isTouch ? 'touchend' : 'mouseup', elementDragEvt = isTouch ? 'touchmove' : 'mousemove'; if (elmMove === undefined) { elmMove = this.elt; elmDrag = elmMove; } else if (elmMove !== this.elt && elmMove.elt !== this.elt) { elmMove = elmMove.elt; elmDrag = this.elt; } elmDrag.addEventListener(dragMouseDownEvt, dragMouseDown, false); elmDrag.style.cursor = 'move'; function dragMouseDown(e) { e = e || window.event; if (isTouch) { const touches = e.changedTouches; px = parseInt(touches[0].clientX); py = parseInt(touches[0].clientY); } else { px = parseInt(e.clientX); py = parseInt(e.clientY); } document.addEventListener(closeDragElementEvt, closeDragElement, false); document.addEventListener(elementDragEvt, elementDrag, false); return false; } function elementDrag(e) { e = e || window.event; if (isTouch) { const touches = e.changedTouches; x = px - parseInt(touches[0].clientX); y = py - parseInt(touches[0].clientY); px = parseInt(touches[0].clientX); py = parseInt(touches[0].clientY); } else { x = px - parseInt(e.clientX); y = py - parseInt(e.clientY); px = parseInt(e.clientX); py = parseInt(e.clientY); } elmMove.style.left = elmMove.offsetLeft - x + 'px'; elmMove.style.top = elmMove.offsetTop - y + 'px'; } function closeDragElement() { document.removeEventListener( closeDragElementEvt, closeDragElement, false ); document.removeEventListener(elementDragEvt, elementDrag, false); } return this; } /** * * @private * @static * @param {String} ev * @param {Boolean|Function} fxn * @param {Element} ctx * @chainable * @alt * General handler for event attaching and detaching */ static _adjustListener(ev, fxn, ctx) { if (fxn === false) { Element._detachListener(ev, ctx); } else { Element._attachListener(ev, fxn, ctx); } return this; } /** * * @private * @static * @param {String} ev * @param {Function} fxn * @param {Element} ctx */ static _attachListener(ev, fxn, ctx) { // detach the old listener if there was one if (ctx._events[ev]) { Element._detachListener(ev, ctx); } const f = fxn.bind(ctx); ctx.elt.addEventListener(ev, f, { capture: false, signal: ctx._pInst._removeSignal }); ctx._events[ev] = f; } /** * * @private * @static * @param {String} ev * @param {Element} ctx */ static _detachListener(ev, ctx) { const f = ctx._events[ev]; ctx.elt.removeEventListener(ev, f, false); ctx._events[ev] = null; } } function element(p5, fn){ /** * A class to describe an * HTML element. * * Sketches can use many elements. Common elements include the drawing canvas, * buttons, sliders, webcam feeds, and so on. * * All elements share the methods of the `p5.Element` class. They're created * with functions such as createCanvas() and * createButton(). * * @class p5.Element * @param {HTMLElement} elt wrapped DOM element. * @param {p5} [pInst] pointer to p5 instance. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a button element and * // place it beneath the canvas. * let btn = createButton('change'); * btn.position(0, 100); * * // Call randomColor() when * // the button is pressed. * btn.mousePressed(randomColor); * * describe('A gray square with a button that says "change" beneath it. The square changes color when the user presses the button.'); * } * * // Paint the background either * // red, yellow, blue, or green. * function randomColor() { * let c = random(['red', 'yellow', 'blue', 'green']); * background(c); * } */ p5.Element = Element; /** * A `Number` property that stores the element's width. * * @type {Number} * @property width * @for p5.Element */ /** * A `Number` property that stores the element's height. * * @type {Number} * @property height * @for p5.Element */ /** * The element's underlying `HTMLElement` object. * * The * HTMLElement * object's properties and methods can be used directly. * * @example * function setup() { * // Create a canvas element and * // assign it to cnv. * let cnv = createCanvas(100, 100); * * background(200); * * // Set the border style for the * // canvas. * cnv.elt.style.border = '5px dashed deeppink'; * * describe('A gray square with a pink border drawn with dashed lines.'); * } * * @type {HTMLElement} * @property elt * @for p5.Element * @name elt * @readOnly */ } if(typeof p5 !== 'undefined'){ element(p5); } /* * This module defines the filters for use with image buffers. * * This module is basically a collection of functions stored in an object * as opposed to modules. The functions are destructive, modifying * the passed in canvas rather than creating a copy. * * Generally speaking users of this module will use the Filters.apply method * on a canvas to create an effect. * * A number of functions are borrowed/adapted from * http://www.html5rocks.com/en/tutorials/canvas/imagefilters/ * or the java processing implementation. * * @private */ const Filters = { /* * Helper functions */ /** * Returns the pixel buffer for a canvas. * * @private * * @param {Canvas|ImageData} canvas the canvas to get pixels from * @return {Uint8ClampedArray} a one-dimensional array containing * the data in the RGBA order, with integer * values between 0 and 255. */ _toPixels(canvas) { // Return pixel data if 'canvas' is an ImageData object. if (canvas instanceof ImageData) { return canvas.data; } else { // Check 2D context support. if (canvas.getContext('2d')) { // Retrieve pixel data. return canvas .getContext('2d') .getImageData(0, 0, canvas.width, canvas.height).data; } else if (canvas.getContext('webgl')) { //Check WebGL context support const gl = canvas.getContext('webgl'); // Calculate the size of pixel data // (4 bytes per pixel - one byte for each RGBA channel). const len = gl.drawingBufferWidth * gl.drawingBufferHeight * 4; const data = new Uint8Array(len); // Use gl.readPixels to fetch pixel data from the WebGL // canvas, storing it in the data array as UNSIGNED_BYTE integers. gl.readPixels( 0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, data ); return data; } } }, /** * Returns a 32-bit number containing ARGB data at the ith pixel in the * 1D array containing pixels data. * * @private * * @param {Uint8ClampedArray} data array returned by _toPixels() * @param {Integer} i index of a 1D Image Array * @return {Integer} 32-bit integer value representing * ARGB value. */ _getARGB(data, i) { // Determine the starting position in the 'data' array for the 'i'-th pixel. const offset = i * 4; return ( // Combining the extracted components using bitwise OR operations to form the final ARGB value. ((data[offset + 3] << 24) & 0xff000000) | //Extract alpha component ((data[offset] << 16) & 0x00ff0000) | //Extract Red component ((data[offset + 1] << 8) & 0x0000ff00) | //Extract green component (data[offset + 2] & 0x000000ff) //Extract blue component ); }, /** * Modifies pixels RGBA values to values contained in the data object. * * @private * * @param {Uint8ClampedArray} pixels array returned by _toPixels() * @param {Int32Array} data source 1D array where each value * represents ARGB values */ _setPixels(pixels, data) { let offset = 0; for (let i = 0, al = pixels.length; i < al; i++) { offset = i * 4; pixels[offset + 0] = (data[i] & 0x00ff0000) >>> 16; pixels[offset + 1] = (data[i] & 0x0000ff00) >>> 8; pixels[offset + 2] = data[i] & 0x000000ff; pixels[offset + 3] = (data[i] & 0xff000000) >>> 24; } }, /** * Returns the ImageData object for a canvas. * https://developer.mozilla.org/en-US/docs/Web/API/ImageData * * @private * * @param {Canvas|ImageData} canvas canvas to get image data from * @return {ImageData} Holder of pixel data (and width and * height) for a canvas */ _toImageData(canvas) { if (canvas instanceof ImageData) { return canvas; } else { return canvas .getContext('2d') .getImageData(0, 0, canvas.width, canvas.height); } }, /** * Returns a blank ImageData object. * * @private * * @param {Integer} width * @param {Integer} height * @return {ImageData} */ _createImageData(width, height) { Filters._tmpCanvas = document.createElement('canvas'); Filters._tmpCtx = Filters._tmpCanvas.getContext('2d'); return this._tmpCtx.createImageData(width, height); }, /** * Applys a filter function to a canvas. * * The difference between this and the actual filter functions defined below * is that the filter functions generally modify the pixel buffer but do * not actually put that data back to the canvas (where it would actually * update what is visible). By contrast this method does make the changes * actually visible in the canvas. * * The apply method is the method that callers of this module would generally * use. It has been separated from the actual filters to support an advanced * use case of creating a filter chain that executes without actually updating * the canvas in between everystep. * * @private * @param {HTMLCanvasElement} canvas The input canvas to apply the filter on. * @param {function(ImageData,Object)} func The filter function to apply to the canvas's pixel data. * @param {Object} filterParam An optional parameter to pass to the filter function. */ apply(canvas, func, filterParam) { const pixelsState = canvas.getContext('2d'); const imageData = pixelsState.getImageData( 0, 0, canvas.width, canvas.height); //Filters can either return a new ImageData object, or just modify //the one they received. const newImageData = func(imageData, filterParam); //If new ImageData is returned, replace the canvas's pixel data with it. if (newImageData instanceof ImageData) { pixelsState.putImageData( newImageData, 0, 0, 0, 0, canvas.width, canvas.height ); } else { //Restore the original pixel. pixelsState.putImageData( imageData, 0, 0, 0, 0, canvas.width, canvas.height ); } }, /* * Filters */ /** * Converts the image to black and white pixels depending if they are above or * below the threshold defined by the level parameter. The parameter must be * between 0.0 (black) and 1.0 (white). If no level is specified, 0.5 is used. * * Borrowed from http://www.html5rocks.com/en/tutorials/canvas/imagefilters/ * * @private * @param {Canvas} canvas Canvas to apply thershold filter on. * @param {Float} level Threshold level (0-1). */ threshold(canvas, level = 0.5) { const pixels = Filters._toPixels(canvas); // Calculate threshold value on a (0-255) scale. const thresh = Math.floor(level * 255); for (let i = 0; i < pixels.length; i += 4) { const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; // CIE luminance for RGB const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b; let val; if (gray >= thresh) { val = 255; } else { val = 0; } pixels[i] = pixels[i + 1] = pixels[i + 2] = val; //set pixel to val. } }, /** * Converts any colors in the image to grayscale equivalents. * No parameter is used. * * Borrowed from http://www.html5rocks.com/en/tutorials/canvas/imagefilters/ * * @private * @param {Canvas} canvas Canvas to apply gray filter on. */ gray(canvas) { const pixels = Filters._toPixels(canvas); for (let i = 0; i < pixels.length; i += 4) { const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; // CIE luminance for RGB const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b; pixels[i] = pixels[i + 1] = pixels[i + 2] = gray; // set pixel to gray. } }, /** * Sets the alpha channel to entirely opaque. No parameter is used. * * @private * @param {Canvas} canvas */ opaque(canvas) { const pixels = Filters._toPixels(canvas); for (let i = 0; i < pixels.length; i += 4) { pixels[i + 3] = 255; } return pixels; }, /** * Sets each pixel to its inverse value. No parameter is used. * @private * @param {Canvas} canvas */ invert(canvas) { const pixels = Filters._toPixels(canvas); for (let i = 0; i < pixels.length; i += 4) { pixels[i] = 255 - pixels[i]; pixels[i + 1] = 255 - pixels[i + 1]; pixels[i + 2] = 255 - pixels[i + 2]; } }, /** * Limits each channel of the image to the number of colors specified as * the parameter. The parameter can be set to values between 2 and 255, but * results are most noticeable in the lower ranges. * * Adapted from java based processing implementation * * @private * @param {Canvas} canvas * @param {Integer} level */ posterize(canvas, level = 4) { const pixels = Filters._toPixels(canvas); if (level < 2 || level > 255) { throw new Error( 'Level must be greater than 2 and less than 255 for posterize' ); } const levels1 = level - 1; for (let i = 0; i < pixels.length; i += 4) { const rlevel = pixels[i]; const glevel = pixels[i + 1]; const blevel = pixels[i + 2]; // New pixel value by posterizing each color. pixels[i] = ((rlevel * level) >> 8) * 255 / levels1; pixels[i + 1] = ((glevel * level) >> 8) * 255 / levels1; pixels[i + 2] = ((blevel * level) >> 8) * 255 / levels1; } }, /** * Increases the bright areas in an image. * @private * @param {Canvas} canvas */ dilate(canvas) { const pixels = Filters._toPixels(canvas); let currIdx = 0; const maxIdx = pixels.length ? pixels.length / 4 : 0; const out = new Int32Array(maxIdx); let currRowIdx, maxRowIdx, colOrig, colOut, currLum; let idxRight, idxLeft, idxUp, idxDown; let colRight, colLeft, colUp, colDown; let lumRight, lumLeft, lumUp, lumDown; // Iterates through rows of pixels. while (currIdx < maxIdx) { currRowIdx = currIdx; maxRowIdx = currIdx + canvas.width; // Iterates through pixels within the current row. while (currIdx < maxRowIdx) { // Get original color of current pixel. colOrig = colOut = Filters._getARGB(pixels, currIdx); idxLeft = currIdx - 1; idxRight = currIdx + 1; idxUp = currIdx - canvas.width; idxDown = currIdx + canvas.width; // Adjust the indices to avoid going out of bounds. if (idxLeft < currRowIdx) { idxLeft = currIdx; } if (idxRight >= maxRowIdx) { idxRight = currIdx; } if (idxUp < 0) { idxUp = 0; } if (idxDown >= maxIdx) { idxDown = currIdx; } colUp = Filters._getARGB(pixels, idxUp); colLeft = Filters._getARGB(pixels, idxLeft); colDown = Filters._getARGB(pixels, idxDown); colRight = Filters._getARGB(pixels, idxRight); // Compute luminance currLum = 77 * ((colOrig >> 16) & 0xff) + 151 * ((colOrig >> 8) & 0xff) + 28 * (colOrig & 0xff); lumLeft = 77 * ((colLeft >> 16) & 0xff) + 151 * ((colLeft >> 8) & 0xff) + 28 * (colLeft & 0xff); lumRight = 77 * ((colRight >> 16) & 0xff) + 151 * ((colRight >> 8) & 0xff) + 28 * (colRight & 0xff); lumUp = 77 * ((colUp >> 16) & 0xff) + 151 * ((colUp >> 8) & 0xff) + 28 * (colUp & 0xff); lumDown = 77 * ((colDown >> 16) & 0xff) + 151 * ((colDown >> 8) & 0xff) + 28 * (colDown & 0xff); // Update the output color based on the highest luminance value if (lumLeft > currLum) { colOut = colLeft; currLum = lumLeft; } if (lumRight > currLum) { colOut = colRight; currLum = lumRight; } if (lumUp > currLum) { colOut = colUp; currLum = lumUp; } if (lumDown > currLum) { colOut = colDown; currLum = lumDown; } // Store the updated color. out[currIdx++] = colOut; } } Filters._setPixels(pixels, out); }, /** * Reduces the bright areas in an image. * Similar to `dilate()`, but updates the output color based on the lowest luminance value. * @private * @param {Canvas} canvas */ erode(canvas) { const pixels = Filters._toPixels(canvas); let currIdx = 0; const maxIdx = pixels.length ? pixels.length / 4 : 0; const out = new Int32Array(maxIdx); let currRowIdx, maxRowIdx, colOrig, colOut, currLum; let idxRight, idxLeft, idxUp, idxDown; let colRight, colLeft, colUp, colDown; let lumRight, lumLeft, lumUp, lumDown; while (currIdx < maxIdx) { currRowIdx = currIdx; maxRowIdx = currIdx + canvas.width; while (currIdx < maxRowIdx) { colOrig = colOut = Filters._getARGB(pixels, currIdx); idxLeft = currIdx - 1; idxRight = currIdx + 1; idxUp = currIdx - canvas.width; idxDown = currIdx + canvas.width; if (idxLeft < currRowIdx) { idxLeft = currIdx; } if (idxRight >= maxRowIdx) { idxRight = currIdx; } if (idxUp < 0) { idxUp = 0; } if (idxDown >= maxIdx) { idxDown = currIdx; } colUp = Filters._getARGB(pixels, idxUp); colLeft = Filters._getARGB(pixels, idxLeft); colDown = Filters._getARGB(pixels, idxDown); colRight = Filters._getARGB(pixels, idxRight); //compute luminance currLum = 77 * ((colOrig >> 16) & 0xff) + 151 * ((colOrig >> 8) & 0xff) + 28 * (colOrig & 0xff); lumLeft = 77 * ((colLeft >> 16) & 0xff) + 151 * ((colLeft >> 8) & 0xff) + 28 * (colLeft & 0xff); lumRight = 77 * ((colRight >> 16) & 0xff) + 151 * ((colRight >> 8) & 0xff) + 28 * (colRight & 0xff); lumUp = 77 * ((colUp >> 16) & 0xff) + 151 * ((colUp >> 8) & 0xff) + 28 * (colUp & 0xff); lumDown = 77 * ((colDown >> 16) & 0xff) + 151 * ((colDown >> 8) & 0xff) + 28 * (colDown & 0xff); if (lumLeft < currLum) { colOut = colLeft; currLum = lumLeft; } if (lumRight < currLum) { colOut = colRight; currLum = lumRight; } if (lumUp < currLum) { colOut = colUp; currLum = lumUp; } if (lumDown < currLum) { colOut = colDown; currLum = lumDown; } // Store the updated color. out[currIdx++] = colOut; } } Filters._setPixels(pixels, out); }, blur(canvas, radius) { blurARGB(canvas, radius); } }; // BLUR // Internal kernel stuff for the gaussian blur filter. let blurRadius; let blurKernelSize; let blurKernel; let blurMult; /* * Port of https://github.com/processing/processing/blob/ * main/core/src/processing/core/PImage.java#L1250 * * Optimized code for building the blur kernel. * further optimized blur code (approx. 15% for radius=20) * bigger speed gains for larger radii (~30%) * added support for various image types (ALPHA, RGB, ARGB) * [toxi 050728] */ function buildBlurKernel(r) { let radius = (r * 3.5) | 0; radius = radius < 1 ? 1 : radius < 248 ? radius : 248; if (blurRadius !== radius) { blurRadius = radius; // Calculating the size of the blur kernel blurKernelSize = (1 + blurRadius) << 1; blurKernel = new Int32Array(blurKernelSize); blurMult = new Array(blurKernelSize); for (let l = 0; l < blurKernelSize; l++) { blurMult[l] = new Int32Array(256); } let bk, bki; let bm, bmi; // Generating blur kernel values. for (let i = 1, radiusi = radius - 1; i < radius; i++) { blurKernel[radius + i] = blurKernel[radiusi] = bki = radiusi * radiusi; bm = blurMult[radius + i]; bmi = blurMult[radiusi--]; for (let j = 0; j < 256; j++) { bm[j] = bmi[j] = bki * j; } } bk = blurKernel[radius] = radius * radius; bm = blurMult[radius]; for (let k = 0; k < 256; k++) { bm[k] = bk * k; } } } // Port of https://github.com/processing/processing/blob/ // main/core/src/processing/core/PImage.java#L1433 function blurARGB(canvas, radius) { // Get pixel data. const pixels = Filters._toPixels(canvas); const width = canvas.width; const height = canvas.height; const numPackedPixels = width * height; const argb = new Int32Array(numPackedPixels); for (let j = 0; j < numPackedPixels; j++) { argb[j] = Filters._getARGB(pixels, j); } let sum, cr, cg, cb, ca; let read, ri, ym, ymi, bk0; const a2 = new Int32Array(numPackedPixels); const r2 = new Int32Array(numPackedPixels); const g2 = new Int32Array(numPackedPixels); const b2 = new Int32Array(numPackedPixels); let yi = 0; buildBlurKernel(radius); let x, y, i; let bm; // Horizontal pass. for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { cb = cg = cr = ca = sum = 0; read = x - blurRadius; // Handle edge cases. if (read < 0) { bk0 = -read; read = 0; } else { if (read >= width) { break; } bk0 = 0; } for (i = bk0; i < blurKernelSize; i++) { if (read >= width) { break; } const c = argb[read + yi]; bm = blurMult[i]; ca += bm[(c & -16777216) >>> 24]; cr += bm[(c & 16711680) >> 16]; cg += bm[(c & 65280) >> 8]; cb += bm[c & 255]; sum += blurKernel[i]; read++; } ri = yi + x; a2[ri] = ca / sum; r2[ri] = cr / sum; g2[ri] = cg / sum; b2[ri] = cb / sum; } yi += width; } yi = 0; ym = -blurRadius; ymi = ym * width; // Vertical pass. for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { cb = cg = cr = ca = sum = 0; // Handle edge cases. if (ym < 0) { bk0 = ri = -ym; read = x; } else { if (ym >= height) { break; } bk0 = 0; ri = ym; read = x + ymi; } for (i = bk0; i < blurKernelSize; i++) { if (ri >= height) { break; } bm = blurMult[i]; ca += bm[a2[read]]; cr += bm[r2[read]]; cg += bm[g2[read]]; cb += bm[b2[read]]; sum += blurKernel[i]; ri++; read += width; } // Set final ARGB value argb[x + yi] = ((ca / sum) << 24) | ((cr / sum) << 16) | ((cg / sum) << 8) | (cb / sum); } yi += width; ymi += width; ym++; } Filters._setPixels(pixels, argb); } function downloadFile(data, fName, extension) { const fx = _checkFileExtension(fName, extension); const filename = fx[0]; let saveData = data; if (!(saveData instanceof Blob)) { saveData = new Blob([data]); } if(document){ const url = URL.createObjectURL(saveData); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); } } function _checkFileExtension(filename, extension) { if (!extension || extension === true || extension === 'true') { extension = ''; } if (!filename) { filename = 'untitled'; } let ext = ''; // make sure the file will have a name, see if filename needs extension if (filename && filename.includes('.')) { ext = filename.split('.').pop(); } // append extension if it doesn't exist if (extension) { if (ext !== extension) { ext = extension; filename = `${filename}.${ext}`; } } return [filename, ext]; } /** * @module Image * @submodule Image * @requires core * @requires constants * @requires filters */ class Image { constructor(width, height) { this.width = width; this.height = height; this.canvas = document.createElement('canvas'); this.canvas.width = this.width; this.canvas.height = this.height; this.drawingContext = this.canvas.getContext('2d'); this._pixelsState = this; this._pixelDensity = 1; //Object for working with GIFs, defaults to null this.gifProperties = null; //For WebGL Texturing only: used to determine whether to reupload texture to GPU this._modified = false; this.pixels = []; } /** * Gets or sets the pixel density for high pixel density displays. * * By default, the density will be set to 1. * * Call this method with no arguments to get the default density, or pass * in a number to set the density. If a non-positive number is provided, * it defaults to 1. * * @param {Number} [density] A scaling factor for the number of pixels per * side * @returns {Number} The current density if called without arguments, or the instance for chaining if setting density. */ pixelDensity(density) { if (typeof density !== 'undefined') { // Setter: set the density and handle resize if (density <= 0) { // p5._friendlyParamError(errorObj, 'pixelDensity'); // Default to 1 in case of an invalid value density = 1; } this._pixelDensity = density; // Adjust canvas dimensions based on pixel density this.width /= density; this.height /= density; return this; // Return the image instance for chaining if needed } else { // Getter: return the default density return this._pixelDensity; } } /** * Helper function for animating GIF-based images with time */ _animateGif(pInst) { const props = this.gifProperties; const curTime = pInst._lastRealFrameTime || window.performance.now(); if (props.lastChangeTime === 0) { props.lastChangeTime = curTime; } if (props.playing) { props.timeDisplayed = curTime - props.lastChangeTime; const curDelay = props.frames[props.displayIndex].delay; if (props.timeDisplayed >= curDelay) { //GIF is bound to 'realtime' so can skip frames const skips = Math.floor(props.timeDisplayed / curDelay); props.timeDisplayed = 0; props.lastChangeTime = curTime; props.displayIndex += skips; props.loopCount = Math.floor(props.displayIndex / props.numFrames); if (props.loopLimit !== null && props.loopCount >= props.loopLimit) { props.playing = false; } else { const ind = props.displayIndex % props.numFrames; this.drawingContext.putImageData(props.frames[ind].image, 0, 0); props.displayIndex = ind; this.setModified(true); } } } } /** * Loads the current value of each pixel in the image into the `img.pixels` * array. * * `img.loadPixels()` must be called before reading or modifying pixel * values. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels. * img.loadPixels(); * * // Set the pixels to black. * for (let x = 0; x < img.width; x += 1) { * for (let y = 0; y < img.height; y += 1) { * img.set(x, y, 0); * } * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels. * img.loadPixels(); * * for (let i = 0; i < img.pixels.length; i += 4) { * // Red. * img.pixels[i] = 0; * // Green. * img.pixels[i + 1] = 0; * // Blue. * img.pixels[i + 2] = 0; * // Alpha. * img.pixels[i + 3] = 255; * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } */ loadPixels() { // Renderer2D.prototype.loadPixels.call(this); const pixelsState = this._pixelsState; const pd = this._pixelDensity; const w = this.width * pd; const h = this.height * pd; const imageData = this.drawingContext.getImageData(0, 0, w, h); // @todo this should actually set pixels per object, so diff buffers can // have diff pixel arrays. pixelsState.imageData = imageData; this.pixels = pixelsState.pixels = imageData.data; this.setModified(true); } /** * Updates the canvas with the RGBA values in the * img.pixels array. * * `img.updatePixels()` only needs to be called after changing values in * the img.pixels array. Such changes can be * made directly after calling * img.loadPixels() or by calling * img.set(). * * The optional parameters `x`, `y`, `width`, and `height` define a * subsection of the image to update. Doing so can improve performance in * some cases. * * If the image was loaded from a GIF, then calling `img.updatePixels()` * will update the pixels in current frame. * * @param {Integer} [x] x-coordinate of the upper-left corner * of the subsection to update. * @param {Integer} [y] y-coordinate of the upper-left corner * of the subsection to update. * @param {Integer} [w] width of the subsection to update. * @param {Integer} [h] height of the subsection to update. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels. * img.loadPixels(); * * // Set the pixels to black. * for (let x = 0; x < img.width; x += 1) { * for (let y = 0; y < img.height; y += 1) { * img.set(x, y, 0); * } * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels. * img.loadPixels(); * * // Set the pixels to black. * for (let i = 0; i < img.pixels.length; i += 4) { * // Red. * img.pixels[i] = 0; * // Green. * img.pixels[i + 1] = 0; * // Blue. * img.pixels[i + 2] = 0; * // Alpha. * img.pixels[i + 3] = 255; * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } */ updatePixels(x, y, w, h) { // Renderer2D.prototype.updatePixels.call(this, x, y, w, h); const pixelsState = this._pixelsState; const pd = this._pixelDensity; if ( x === undefined && y === undefined && w === undefined && h === undefined ) { x = 0; y = 0; w = this.width; h = this.height; } x *= pd; y *= pd; w *= pd; h *= pd; if (this.gifProperties) { this.gifProperties.frames[this.gifProperties.displayIndex].image = pixelsState.imageData; } this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); this.setModified(true); } /** * Gets a pixel or a region of pixels from the image. * * `img.get()` is easy to use but it's not as fast as * img.pixels. Use * img.pixels to read many pixel values. * * The version of `img.get()` with no parameters returns the entire image. * * The version of `img.get()` with two parameters, as in `img.get(10, 20)`, * interprets them as coordinates. It returns an array with the * `[R, G, B, A]` values of the pixel at the given point. * * The version of `img.get()` with four parameters, as in * `img,get(10, 20, 50, 90)`, interprets them as * coordinates and dimensions. The first two parameters are the coordinates * of the upper-left corner of the subsection. The last two parameters are * the width and height of the subsection. It returns a subsection of the * canvas in a new p5.Image object. * * Use `img.get()` instead of get() to work directly * with images. * * @param {Number} x x-coordinate of the pixel. * @param {Number} y y-coordinate of the pixel. * @param {Number} w width of the subsection to be returned. * @param {Number} h height of the subsection to be returned. * @return {p5.Image} subsection as a p5.Image object. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * background(200); * * // Display the image. * image(img, 0, 0); * * // Copy the image. * let img2 = get(); * * // Display the copied image on the right. * image(img2, 50, 0); * * describe('Two identical mountain landscapes shown side-by-side.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Get a pixel's color. * let c = img.get(50, 90); * * // Style the square using the pixel's color. * fill(c); * noStroke(); * * // Draw the square. * square(25, 25, 50); * * describe('A mountain landscape with an olive green square in its center.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Copy half of the image. * let img2 = img.get(0, 0, img.width / 2, img.height / 2); * * // Display half of the image. * image(img2, 50, 50); * * describe('A mountain landscape drawn on top of another mountain landscape.'); * } */ /** * @return {p5.Image} whole p5.Image */ /** * @param {Number} x * @param {Number} y * @return {Number[]} color of the pixel at (x, y) in array format `[R, G, B, A]`. */ get(x, y, w, h) { // p5._validateParameters('p5.Image.get', arguments); // return Renderer2D.prototype.get.apply(this, arguments); const pixelsState = this._pixelsState; const pd = this._pixelDensity; const canvas = this.canvas; if (typeof x === 'undefined' && typeof y === 'undefined') { // get() x = y = 0; w = pixelsState.width; h = pixelsState.height; } else { x *= pd; y *= pd; if (typeof w === 'undefined' && typeof h === 'undefined') { // get(x,y) if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) { return [0, 0, 0, 0]; } return this._getPixel(x, y); } // get(x,y,w,h) } const region = new Image(w*pd, h*pd); region.pixelDensity(pd); region.canvas .getContext('2d') .drawImage(canvas, x, y, w * pd, h * pd, 0, 0, w*pd, h*pd); return region; } _getPixel(x, y) { let imageData, index; imageData = this.drawingContext.getImageData(x, y, 1, 1).data; index = 0; return [ imageData[index + 0], imageData[index + 1], imageData[index + 2], imageData[index + 3] ]; // return Renderer2D.prototype._getPixel.apply(this, args); } /** * Sets the color of one or more pixels within an image. * * `img.set()` is easy to use but it's not as fast as * img.pixels. Use * img.pixels to set many pixel values. * * `img.set()` interprets the first two parameters as x- and y-coordinates. It * interprets the last parameter as a grayscale value, a `[R, G, B, A]` pixel * array, a p5.Color object, or another * p5.Image object. * * img.updatePixels() must be called * after using `img.set()` for changes to appear. * * @param {Number} x x-coordinate of the pixel. * @param {Number} y y-coordinate of the pixel. * @param {Number|Number[]|Object} a grayscale value | pixel array | * p5.Color object | * p5.Image to copy. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(100, 100); * * // Set four pixels to black. * img.set(30, 20, 0); * img.set(85, 20, 0); * img.set(85, 75, 0); * img.set(30, 75, 0); * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 0, 0); * * describe('Four black dots arranged in a square drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(100, 100); * * // Create a p5.Color object. * let black = color(0); * * // Set four pixels to black. * img.set(30, 20, black); * img.set(85, 20, black); * img.set(85, 75, black); * img.set(30, 75, black); * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 0, 0); * * describe('Four black dots arranged in a square drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Draw a color gradient. * for (let x = 0; x < img.width; x += 1) { * for (let y = 0; y < img.height; y += 1) { * let c = map(x, 0, img.width, 0, 255); * img.set(x, y, c); * } * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A square with a horiztonal color gradient from black to white drawn on a gray background.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Create a p5.Image object. * let img2 = createImage(100, 100); * * // Set the blank image's pixels using the landscape. * img2.set(0, 0, img); * * // Display the second image. * image(img2, 0, 0); * * describe('An image of a mountain landscape.'); * } */ set(x, y, imgOrCol) { // Renderer2D.prototype.set.call(this, x, y, imgOrCol); // round down to get integer numbers x = Math.floor(x); y = Math.floor(y); const pixelsState = this._pixelsState; if (imgOrCol instanceof Image) { this.drawingContext.save(); this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); this.drawingContext.scale( this._pixelDensity, this._pixelDensity ); this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); this.drawingContext.drawImage(imgOrCol.canvas, x, y); this.drawingContext.restore(); } else { let r = 0, g = 0, b = 0, a = 0; let idx = 4 * (y * this._pixelDensity * (this.width * this._pixelDensity) + x * this._pixelDensity); if (!pixelsState.imageData) { pixelsState.loadPixels(); } if (typeof imgOrCol === 'number') { if (idx < pixelsState.pixels.length) { r = imgOrCol; g = imgOrCol; b = imgOrCol; a = 255; //this.updatePixels.call(this); } } else if (Array.isArray(imgOrCol)) { if (imgOrCol.length < 4) { throw new Error('pixel array must be of the form [R, G, B, A]'); } if (idx < pixelsState.pixels.length) { r = imgOrCol[0]; g = imgOrCol[1]; b = imgOrCol[2]; a = imgOrCol[3]; //this.updatePixels.call(this); } } else if (imgOrCol instanceof p5.Color) { if (idx < pixelsState.pixels.length) { [r, g, b, a] = imgOrCol._getRGBA([255, 255, 255, 255]); //this.updatePixels.call(this); } } // loop over pixelDensity * pixelDensity for (let i = 0; i < this._pixelDensity; i++) { for (let j = 0; j < this._pixelDensity; j++) { // loop over idx = 4 * ((y * this._pixelDensity + j) * this.width * this._pixelDensity + (x * this._pixelDensity + i)); pixelsState.pixels[idx] = r; pixelsState.pixels[idx + 1] = g; pixelsState.pixels[idx + 2] = b; pixelsState.pixels[idx + 3] = a; } } } this.setModified(true); } /** * Resizes the image to a given width and height. * * The image's original aspect ratio can be kept by passing 0 for either * `width` or `height`. For example, calling `img.resize(50, 0)` on an image * that was 500 × 300 pixels will resize it to 50 × 30 pixels. * * @param {Number} width resized image width. * @param {Number} height resized image height. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Resize the image. * img.resize(50, 100); * * // Display the resized image. * image(img, 0, 0); * * describe('Two images of a mountain landscape. One copy of the image is squeezed horizontally.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Resize the image, keeping the aspect ratio. * img.resize(0, 30); * * // Display the resized image. * image(img, 0, 0); * * describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Resize the image, keeping the aspect ratio. * img.resize(60, 0); * * // Display the image. * image(img, 0, 0); * * describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.'); * } */ resize(width, height) { // Copy contents to a temporary canvas, resize the original // and then copy back. // // There is a faster approach that involves just one copy and swapping the // this.canvas reference. We could switch to that approach if (as i think // is the case) there an expectation that the user would not hold a // reference to the backing canvas of a p5.Image. But since we do not // enforce that at the moment, I am leaving in the slower, but safer // implementation. // auto-resize if (width === 0 && height === 0) { width = this.canvas.width; height = this.canvas.height; } else if (width === 0) { width = this.canvas.width * height / this.canvas.height; } else if (height === 0) { height = this.canvas.height * width / this.canvas.width; } width = Math.floor(width); height = Math.floor(height); const tempCanvas = document.createElement('canvas'); tempCanvas.width = width; tempCanvas.height = height; if (this.gifProperties) { const props = this.gifProperties; //adapted from github.com/LinusU/resize-image-data const nearestNeighbor = (src, dst) => { let pos = 0; for (let y = 0; y < dst.height; y++) { for (let x = 0; x < dst.width; x++) { const srcX = Math.floor(x * src.width / dst.width); const srcY = Math.floor(y * src.height / dst.height); let srcPos = (srcY * src.width + srcX) * 4; dst.data[pos++] = src.data[srcPos++]; // R dst.data[pos++] = src.data[srcPos++]; // G dst.data[pos++] = src.data[srcPos++]; // B dst.data[pos++] = src.data[srcPos++]; // A } } }; for (let i = 0; i < props.numFrames; i++) { const resizedImageData = this.drawingContext.createImageData( width, height ); nearestNeighbor(props.frames[i].image, resizedImageData); props.frames[i].image = resizedImageData; } } tempCanvas.getContext('2d').drawImage( this.canvas, 0, 0, this.canvas.width, this.canvas.height, 0, 0, tempCanvas.width, tempCanvas.height ); // Resize the original canvas, which will clear its contents this.canvas.width = this.width = width; this.canvas.height = this.height = height; //Copy the image back this.drawingContext.drawImage( tempCanvas, 0, 0, width, height, 0, 0, width, height ); if (this.pixels.length > 0) { this.loadPixels(); } this.setModified(true); } /** * Copies pixels from a source image to this image. * * The first parameter, `srcImage`, is an optional * p5.Image object to copy. If a source image isn't * passed, then `img.copy()` can copy a region of this image to another * region. * * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region * to copy from the source image. `(sx, sy)` is the top-left corner of the * region. `sw` and `sh` are the region's width and height. * * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region * of this image to copy into. `(dx, dy)` is the top-left corner of the * region. `dw` and `dh` are the region's width and height. * * Calling `img.copy()` will scale pixels from the source region if it isn't * the same size as the destination region. * * @param {p5.Image|p5.Element} srcImage source image. * @param {Integer} sx x-coordinate of the source's upper-left corner. * @param {Integer} sy y-coordinate of the source's upper-left corner. * @param {Integer} sw source image width. * @param {Integer} sh source image height. * @param {Integer} dx x-coordinate of the destination's upper-left corner. * @param {Integer} dy y-coordinate of the destination's upper-left corner. * @param {Integer} dw destination image width. * @param {Integer} dh destination image height. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Copy one region of the image to another. * img.copy(7, 22, 10, 10, 35, 25, 50, 50); * * // Display the image. * image(img, 0, 0); * * // Outline the copied region. * stroke(255); * noFill(); * square(7, 22, 10); * * describe('An image of a mountain landscape. A square region is outlined in white. A larger square contains a pixelated view of the outlined region.'); * } * * @example * let mountains; * let bricks; * * async function setup() { * // Load the images. * mountains = await loadImage('assets/rockies.jpg'); * bricks = await loadImage('assets/bricks.jpg'); * createCanvas(100, 100); * * // Calculate the center of the bricks image. * let x = bricks.width / 2; * let y = bricks.height / 2; * * // Copy the bricks to the mountains image. * mountains.copy(bricks, 0, 0, x, y, 0, 0, x, y); * * // Display the mountains image. * image(mountains, 0, 0); * * describe('An image of a brick wall drawn at the top-left of an image of a mountain landscape.'); * } */ /** * @param {Integer} sx * @param {Integer} sy * @param {Integer} sw * @param {Integer} sh * @param {Integer} dx * @param {Integer} dy * @param {Integer} dw * @param {Integer} dh */ copy(...args) { // NOTE: Duplicate implementation here and pixels.js let srcImage, sx, sy, sw, sh, dx, dy, dw, dh; if (args.length === 9) { srcImage = args[0]; sx = args[1]; sy = args[2]; sw = args[3]; sh = args[4]; dx = args[5]; dy = args[6]; dw = args[7]; dh = args[8]; } else if (args.length === 8) { srcImage = this; sx = args[0]; sy = args[1]; sw = args[2]; sh = args[3]; dx = args[4]; dy = args[5]; dw = args[6]; dh = args[7]; } else { throw new Error('Signature not supported'); } this._copyHelper(this, srcImage, sx, sy, sw, sh, dx, dy, dw, dh); } _copyHelper( dstImage, srcImage, sx, sy, sw, sh, dx, dy, dw, dh ){ const s = srcImage.canvas.width / srcImage.width; // adjust coord system for 3D when renderer // ie top-left = -width/2, -height/2 let sxMod = 0; let syMod = 0; if (srcImage._renderer && srcImage._renderer.isP3D) { sxMod = srcImage.width / 2; syMod = srcImage.height / 2; } if (dstImage._renderer && dstImage._renderer.isP3D) { dstImage.push(); dstImage.resetMatrix(); dstImage.noLights(); dstImage.blendMode(dstImage.BLEND); dstImage.imageMode(dstImage.CORNER); dstImage._renderer.image( srcImage, sx + sxMod, sy + syMod, sw, sh, dx, dy, dw, dh ); dstImage.pop(); } else { dstImage.drawingContext.drawImage( srcImage.canvas, s * (sx + sxMod), s * (sy + syMod), s * sw, s * sh, dx, dy, dw, dh ); } } /** * Masks part of the image with another. * * `img.mask()` uses another p5.Image object's * alpha channel as the alpha channel for this image. Masks are cumulative * and can't be removed once applied. If the mask has a different * pixel density from this image, the mask will be scaled. * * @param {p5.Image} srcImage source image. * * @example * let photo; * let maskImage; * * async function setup() { * // Load the images. * photo = await loadImage('assets/rockies.jpg'); * maskImage = await loadImage('assets/mask2.png'); * createCanvas(100, 100); * * // Apply the mask. * photo.mask(maskImage); * * // Display the image. * image(photo, 0, 0); * * describe('An image of a mountain landscape. The right side of the image has a faded patch of white.'); * } */ // TODO: - Accept an array of alpha values. mask(p5Image) { if (p5Image === undefined) { p5Image = this; } const currBlend = this.drawingContext.globalCompositeOperation; let imgScaleFactor = this._pixelDensity; let maskScaleFactor = 1; if (p5Image instanceof Renderer) { maskScaleFactor = p5Image._pInst._renderer._pixelDensity; } const copyArgs = [ p5Image, 0, 0, maskScaleFactor * p5Image.width, maskScaleFactor * p5Image.height, 0, 0, imgScaleFactor * this.width, imgScaleFactor * this.height ]; this.drawingContext.globalCompositeOperation = 'destination-in'; if (this.gifProperties) { for (let i = 0; i < this.gifProperties.frames.length; i++) { this.drawingContext.putImageData( this.gifProperties.frames[i].image, 0, 0 ); this.copy(...copyArgs); this.gifProperties.frames[i].image = this.drawingContext.getImageData( 0, 0, imgScaleFactor * this.width, imgScaleFactor * this.height ); } this.drawingContext.putImageData( this.gifProperties.frames[this.gifProperties.displayIndex].image, 0, 0 ); } else { this.copy(...copyArgs); } this.drawingContext.globalCompositeOperation = currBlend; this.setModified(true); } /** * Applies an image filter to the image. * * The preset options are: * * `INVERT` * Inverts the colors in the image. No parameter is used. * * `GRAY` * Converts the image to grayscale. No parameter is used. * * `THRESHOLD` * Converts the image to black and white. Pixels with a grayscale value * above a given threshold are converted to white. The rest are converted to * black. The threshold must be between 0.0 (black) and 1.0 (white). If no * value is specified, 0.5 is used. * * `OPAQUE` * Sets the alpha channel to be entirely opaque. No parameter is used. * * `POSTERIZE` * Limits the number of colors in the image. Each color channel is limited to * the number of colors specified. Values between 2 and 255 are valid, but * results are most noticeable with lower values. The default value is 4. * * `BLUR` * Blurs the image. The level of blurring is specified by a blur radius. Larger * values increase the blur. The default value is 4. A gaussian blur is used * in `P2D` mode. A box blur is used in `WEBGL` mode. * * `ERODE` * Reduces the light areas. No parameter is used. * * `DILATE` * Increases the light areas. No parameter is used. * * @param {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|ERODE|DILATE|BLUR)} filterType either THRESHOLD, GRAY, OPAQUE, INVERT, * POSTERIZE, ERODE, DILATE or BLUR. * @param {Number} [filterParam] parameter unique to each filter. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the INVERT filter. * img.filter(INVERT); * * // Display the image. * image(img, 0, 0); * * describe('A blue brick wall.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the GRAY filter. * img.filter(GRAY); * * // Display the image. * image(img, 0, 0); * * describe('A brick wall drawn in grayscale.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the THRESHOLD filter. * img.filter(THRESHOLD); * * // Display the image. * image(img, 0, 0); * * describe('A brick wall drawn in black and white.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the OPAQUE filter. * img.filter(OPAQUE); * * // Display the image. * image(img, 0, 0); * * describe('A red brick wall.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the POSTERIZE filter. * img.filter(POSTERIZE, 3); * * // Display the image. * image(img, 0, 0); * * describe('An image of a red brick wall drawn with a limited color palette.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the BLUR filter. * img.filter(BLUR, 3); * * // Display the image. * image(img, 0, 0); * * describe('A blurry image of a red brick wall.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the DILATE filter. * img.filter(DILATE); * * // Display the image. * image(img, 0, 0); * * describe('A red brick wall with bright lines between each brick.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the ERODE filter. * img.filter(ERODE); * * // Display the image. * image(img, 0, 0); * * describe('A red brick wall with faint lines between each brick.'); * } */ filter(operation, value) { Filters.apply(this.canvas, Filters[operation], value); this.setModified(true); } /** * Copies a region of pixels from another image into this one. * * The first parameter, `srcImage`, is the * p5.Image object to blend. * * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region * to blend from the source image. `(sx, sy)` is the top-left corner of the * region. `sw` and `sh` are the regions width and height. * * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region * of the canvas to blend into. `(dx, dy)` is the top-left corner of the * region. `dw` and `dh` are the regions width and height. * * The tenth parameter, `blendMode`, sets the effect used to blend the images' * colors. The options are `BLEND`, `DARKEST`, `LIGHTEST`, `DIFFERENCE`, * `MULTIPLY`, `EXCLUSION`, `SCREEN`, `REPLACE`, `OVERLAY`, `HARD_LIGHT`, * `SOFT_LIGHT`, `DODGE`, `BURN`, `ADD`, or `NORMAL`. * * @param {p5.Image} srcImage source image * @param {Integer} sx x-coordinate of the source's upper-left corner. * @param {Integer} sy y-coordinate of the source's upper-left corner. * @param {Integer} sw source image width. * @param {Integer} sh source image height. * @param {Integer} dx x-coordinate of the destination's upper-left corner. * @param {Integer} dy y-coordinate of the destination's upper-left corner. * @param {Integer} dw destination image width. * @param {Integer} dh destination image height. * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode the blend mode. either * BLEND, DARKEST, LIGHTEST, DIFFERENCE, * MULTIPLY, EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT, * SOFT_LIGHT, DODGE, BURN, ADD or NORMAL. * * Available blend modes are: normal | multiply | screen | overlay | * darken | lighten | color-dodge | color-burn | hard-light | * soft-light | difference | exclusion | hue | saturation | * color | luminosity * * http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/ * * @example * let mountains; * let bricks; * * async function setup() { * // Load the images. * mountains = await loadImage('assets/rockies.jpg'); * bricks = await loadImage('assets/bricks_third.jpg'); * createCanvas(100, 100); * * // Blend the bricks image into the mountains. * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, ADD); * * // Display the mountains image. * image(mountains, 0, 0); * * // Display the bricks image. * image(bricks, 0, 0); * * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears faded on the right of the image.'); * } * * @example * let mountains; * let bricks; * * async function setup() { * // Load the images. * mountains = await loadImage('assets/rockies.jpg'); * bricks = await loadImage('assets/bricks_third.jpg'); * * createCanvas(100, 100); * * // Blend the bricks image into the mountains. * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, DARKEST); * * // Display the mountains image. * image(mountains, 0, 0); * * // Display the bricks image. * image(bricks, 0, 0); * * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears transparent on the right of the image.'); * } * * @example * let mountains; * let bricks; * * async function setup() { * // Load the images. * mountains = await loadImage('assets/rockies.jpg'); * bricks = await loadImage('assets/bricks_third.jpg'); * * createCanvas(100, 100); * * // Blend the bricks image into the mountains. * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, LIGHTEST); * * // Display the mountains image. * image(mountains, 0, 0); * * // Display the bricks image. * image(bricks, 0, 0); * * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears washed out on the right of the image.'); * } */ /** * @param {Integer} sx * @param {Integer} sy * @param {Integer} sw * @param {Integer} sh * @param {Integer} dx * @param {Integer} dy * @param {Integer} dw * @param {Integer} dh * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode */ blend(...args) { const currBlend = this.drawingContext.globalCompositeOperation; const blendMode = args[args.length - 1]; const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1); this.drawingContext.globalCompositeOperation = blendMode; this.copy(...copyArgs); this.drawingContext.globalCompositeOperation = currBlend; this.setModified(true); } /** * helper method for web GL mode to indicate that an image has been * changed or unchanged since last upload. gl texture upload will * set this value to false after uploading the texture. * @param {Boolean} val sets whether or not the image has been * modified. * @private */ setModified(val) { this._modified = val; //enforce boolean? } /** * helper method for web GL mode to figure out if the image * has been modified and might need to be re-uploaded to texture * memory between frames. * @private * @return {boolean} a boolean indicating whether or not the * image has been updated or modified since last texture upload. */ isModified() { return this._modified; } /** * Saves the image to a file. * * By default, `img.save()` saves the image as a PNG image called * `untitled.png`. * * The first parameter, `filename`, is optional. It's a string that sets the * file's name. If a file extension is included, as in * `img.save('drawing.png')`, then the image will be saved using that * format. * * The second parameter, `extension`, is also optional. It sets the files format. * Either `'png'` or `'jpg'` can be used. For example, `img.save('drawing', 'jpg')` * saves the canvas to a file called `drawing.jpg`. * * Note: The browser will either save the file immediately or prompt the user * with a dialogue window. * * The image will only be downloaded as an animated GIF if it was loaded * from a GIF file. See saveGif() to create new * GIFs. * * @param {String} filename filename. Defaults to 'untitled'. * @param {String} [extension] file extension, either 'png' or 'jpg'. * Defaults to 'png'. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * describe('An image of a mountain landscape. The image is downloaded when the user presses the "s", "j", or "p" key.'); * } * * // Save the image with different options when the user presses a key. * function keyPressed() { * if (key === 's') { * img.save(); * } else if (key === 'j') { * img.save('rockies.jpg'); * } else if (key === 'p') { * img.save('rockies', 'png'); * } * } */ save(filename, extension) { if (this.gifProperties) { encodeAndDownloadGif(this, filename); } else { let htmlCanvas = this.canvas; extension = extension || _checkFileExtension(filename, extension)[1] || 'png'; let mimeType; switch (extension) { default: //case 'png': mimeType = 'image/png'; break; case 'webp': mimeType = 'image/webp'; break; case 'jpeg': case 'jpg': mimeType = 'image/jpeg'; break; } htmlCanvas.toBlob(blob => { downloadFile(blob, filename, extension); }, mimeType); } } async toBlob() { return new Promise(resolve => { this.canvas.toBlob(resolve); }); } // GIF Section /** * Restarts an animated GIF at its first frame. * * @example * let gif; * * async function setup() { * // Load the image. * gif = await loadImage('assets/arnott-wallace-wink-loop-once.gif'); * * createCanvas(100, 100); * * describe('A cartoon face winks once and then freezes. Clicking resets the face and makes it wink again.'); * } * * function draw() { * background(255); * * // Display the image. * image(gif, 0, 0); * } * * // Reset the GIF when the user presses the mouse. * function mousePressed() { * gif.reset(); * } */ reset() { if (this.gifProperties) { const props = this.gifProperties; props.playing = true; props.timeSinceStart = 0; props.timeDisplayed = 0; props.lastChangeTime = 0; props.loopCount = 0; props.displayIndex = 0; this.drawingContext.putImageData(props.frames[0].image, 0, 0); } } /** * Gets the index of the current frame in an animated GIF. * * @return {Number} index of the GIF's current frame. * * @example * let gif; * * async function setup() { * // Load the image. * gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif'); * * createCanvas(100, 100); * * describe('A cartoon eye repeatedly looks around, then outwards. A number displayed in the bottom-left corner increases from 0 to 124, then repeats.'); * } * * function draw() { * // Get the index of the current GIF frame. * let index = gif.getCurrentFrame(); * * // Display the image. * image(gif, 0, 0); * * // Display the current frame. * text(index, 10, 90); * } */ getCurrentFrame() { if (this.gifProperties) { const props = this.gifProperties; return props.displayIndex % props.numFrames; } } /** * Sets the current frame in an animated GIF. * * @param {Number} index index of the frame to display. * * @example * let gif; * let frameSlider; * * async function setup() { * // Load the image. * gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif'); * * createCanvas(100, 100); * * // Get the index of the last frame. * let maxFrame = gif.numFrames() - 1; * * // Create a slider to control which frame is drawn. * frameSlider = createSlider(0, maxFrame); * frameSlider.position(10, 80); * frameSlider.size(80); * * describe('A cartoon eye looks around when a slider is moved.'); * } * * function draw() { * // Get the slider's value. * let index = frameSlider.value(); * * // Set the GIF's frame. * gif.setFrame(index); * * // Display the image. * image(gif, 0, 0); * } */ setFrame(index) { if (this.gifProperties) { const props = this.gifProperties; if (index < props.numFrames && index >= 0) { props.timeDisplayed = 0; props.lastChangeTime = 0; props.displayIndex = index; this.drawingContext.putImageData(props.frames[index].image, 0, 0); } else { console.log( 'Cannot set GIF to a frame number that is higher than total number of frames or below zero.' ); } } } /** * Returns the number of frames in an animated GIF. * * @return {Number} number of frames in the GIF. * * @example * let gif; * * async function setup() { * // Load the image. * gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif'); * * createCanvas(100, 100); * * describe('A cartoon eye looks around. The text "n / 125" is shown at the bottom of the canvas.'); * } * * function draw() { * // Display the image. * image(gif, 0, 0); * * // Display the current state of playback. * let total = gif.numFrames(); * let index = gif.getCurrentFrame(); * text(`${index} / ${total}`, 30, 90); * } */ numFrames() { if (this.gifProperties) { return this.gifProperties.numFrames; } } /** * Plays an animated GIF that was paused with * img.pause(). * * @example * let gif; * * async function setup() { * // Load the image. * gif = await loadImage('assets/nancy-liang-wind-loop-forever.gif'); * * createCanvas(100, 100); * * describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.'); * } * * function draw() { * background(255); * image(gif, 0, 0); * } * * // Pause the GIF when the user presses the mouse. * function mousePressed() { * gif.pause(); * } * * // Play the GIF when the user releases the mouse. * function mouseReleased() { * gif.play(); * } */ play() { if (this.gifProperties) { this.gifProperties.playing = true; } } /** * Pauses an animated GIF. * * The GIF can be resumed by calling * img.play(). * * @example * let gif; * * async function setup() { * // Load the image. * gif = await loadImage('assets/nancy-liang-wind-loop-forever.gif'); * * createCanvas(100, 100); * * describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.'); * } * * function draw() { * background(255); * * // Display the image. * image(gif, 0, 0); * } * * // Pause the GIF when the user presses the mouse. * function mousePressed() { * gif.pause(); * } * * // Play the GIF when the user presses the mouse. * function mouseReleased() { * gif.play(); * } */ pause() { if (this.gifProperties) { this.gifProperties.playing = false; } } /** * Changes the delay between frames in an animated GIF. * * The first parameter, `delay`, is the length of the delay in milliseconds. * * The second parameter, `index`, is optional. If provided, only the frame * at `index` will have its delay modified. All other frames will keep * their default delay. * * @param {Number} d delay in milliseconds between switching frames. * @param {Number} [index] index of the frame that will have its delay modified. * * @example * let gifFast; * let gifSlow; * * async function setup() { * // Load the images. * gifFast = await loadImage('assets/arnott-wallace-eye-loop-forever.gif'); * gifSlow = await loadImage('assets/arnott-wallace-eye-loop-forever.gif'); * * createCanvas(100, 100); * * background(200); * * // Resize the images. * gifFast.resize(50, 50); * gifSlow.resize(50, 50); * * // Set the delay lengths. * gifFast.delay(10); * gifSlow.delay(100); * * describe('Two animated eyes looking around. The eye on the left moves faster than the eye on the right.'); * } * * function draw() { * // Display the images. * image(gifFast, 0, 0); * image(gifSlow, 50, 0); * } * * @example * let gif; * * async function setup() { * // Load the image. * gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif'); * * createCanvas(100, 100); * * // Set the delay of frame 67. * gif.delay(3000, 67); * * describe('An animated eye looking around. It pauses for three seconds while it looks down.'); * } * * function draw() { * // Display the image. * image(gif, 0, 0); * } */ delay(d, index) { if (this.gifProperties) { const props = this.gifProperties; if (index < props.numFrames && index >= 0) { props.frames[index].delay = d; } else { // change all frames for (const frame of props.frames) { frame.delay = d; } } } } } function encodeAndDownloadGif(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(); allFramesPixelColors[i].forEach((color, 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' }); downloadFile(blob, filename, extension); } function image$2(p5, fn){ /** * A class to describe an image. * * Images are rectangular grids of pixels that can be displayed and modified. * * Existing images can be loaded by calling * loadImage(). Blank images can be created by * calling createImage(). `p5.Image` objects * have methods for common tasks such as applying filters and modifying * pixel values. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * describe('An image of a brick wall.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Apply the GRAY filter. * img.filter(GRAY); * * // Display the image. * image(img, 0, 0); * * describe('A grayscale image of a brick wall.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels. * img.loadPixels(); * * // Set the pixels to black. * for (let x = 0; x < img.width; x += 1) { * for (let y = 0; y < img.height; y += 1) { * img.set(x, y, 0); * } * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } * * @class p5.Image * @param {Number} width * @param {Number} height */ p5.Image = Image; /** * The image's width in pixels. * * @type {Number} * @property {Number} width * @for p5.Image * @name width * @readOnly * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Calculate the center coordinates. * let x = img.width / 2; * let y = img.height / 2; * * // Draw a circle at the image's center. * circle(x, y, 20); * * describe('An image of a mountain landscape with a white circle drawn in the middle.'); * } */ /** * The image's height in pixels. * * @type {Number} * @property height * @for p5.Image * @name height * @readOnly * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Calculate the center coordinates. * let x = img.width / 2; * let y = img.height / 2; * * // Draw a circle at the image's center. * circle(x, y, 20); * * describe('An image of a mountain landscape with a white circle drawn in the middle.'); * } */ /** * An array containing the color of each pixel in the image. * * Colors are stored as numbers representing red, green, blue, and alpha * (RGBA) values. `img.pixels` is a one-dimensional array for performance * reasons. * * Each pixel occupies four elements in the pixels array, one for each * RGBA value. For example, the pixel at coordinates (0, 0) stores its * RGBA values at `img.pixels[0]`, `img.pixels[1]`, `img.pixels[2]`, * and `img.pixels[3]`, respectively. The next pixel at coordinates (1, 0) * stores its RGBA values at `img.pixels[4]`, `img.pixels[5]`, * `img.pixels[6]`, and `img.pixels[7]`. And so on. The `img.pixels` array * for a 100×100 p5.Image object has * 100 × 100 × 4 = 40,000 elements. * * Accessing the RGBA values for a pixel in the image requires a little * math as shown in the examples below. The * img.loadPixels() * method must be called before accessing the `img.pixels` array. The * img.updatePixels() method must be * called after any changes are made. * * @property {Number[]} pixels * @for p5.Image * @name pixels * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels. * img.loadPixels(); * * for (let i = 0; i < img.pixels.length; i += 4) { * // Red. * img.pixels[i] = 0; * // Green. * img.pixels[i + 1] = 0; * // Blue. * img.pixels[i + 2] = 0; * // Alpha. * img.pixels[i + 3] = 255; * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels. * img.loadPixels(); * * // Set the pixels to red. * for (let i = 0; i < img.pixels.length; i += 4) { * // Red. * img.pixels[i] = 255; * // Green. * img.pixels[i + 1] = 0; * // Blue. * img.pixels[i + 2] = 0; * // Alpha. * img.pixels[i + 3] = 255; * } * * // Update the image. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A red square drawn in the middle of a gray square.'); * } */ } if(typeof p5 !== 'undefined'){ image$2(p5); } /** * @module Math * @requires constants */ /// HELPERS FOR REMAINDER METHOD const calculateRemainder2D = function (xComponent, yComponent) { if (xComponent !== 0) { this.x = this.x % xComponent; } if (yComponent !== 0) { this.y = this.y % yComponent; } return this; }; const calculateRemainder3D = function (xComponent, yComponent, zComponent) { if (xComponent !== 0) { this.x = this.x % xComponent; } if (yComponent !== 0) { this.y = this.y % yComponent; } if (zComponent !== 0) { this.z = this.z % zComponent; } return this; }; class Vector { // This is how it comes in with createVector() // This check if the first argument is a function constructor(...args) { let values = args; // .map(arg => arg || 0); if (typeof args[0] === 'function') { this.isPInst = true; this._fromRadians = args[0]; this._toRadians = args[1]; values = args.slice(2); // .map(arg => arg || 0); } let dimensions = values.length; // TODO: make default 3 if no arguments if (dimensions === 0) { this.dimensions = 2; this._values = [0, 0, 0]; } else { this.dimensions = dimensions; this._values = values; } } /** * Gets the values of the N-dimensional vector. * * This method returns an array of numbers that represent the vector. * Each number in the array corresponds to a different component of the vector, * like its position in different directions (e.g., x, y, z). * * @returns {Array} The array of values representing the vector. */ get values() { return this._values; } /** * Sets the values of the vector. * * This method allows you to update the entire vector with a new set of values. * You need to provide an array of numbers, where each number represents a component * of the vector (e.g., x, y, z). The length of the array should match the number of * dimensions of the vector. If the array is shorter, the missing components will be * set to 0. If the array is longer, the extra values will be ignored. * * @param {Array} newValues - An array of numbers representing the new values for the vector. * */ set values(newValues) { let dimensions = newValues.length; if (dimensions === 0) { this.dimensions = 2; this._values = [0, 0, 0]; } else { this.dimensions = dimensions; this._values = newValues.slice(); } } /** * Gets the x component of the vector. * * This method returns the value of the x component of the vector. * Think of the x component as the horizontal position or the first number in the vector. * If the x component is not defined, it will return 0. * * @returns {Number} The x component of the vector. Returns 0 if the value is not defined. */ get x() { return this._values[0] || 0; } /** * Retrieves the value at the specified index from the vector. * * This method allows you to get the value of a specific component of the vector * by providing its index. Think of the vector as a list of numbers, where each * number represents a different direction (like x, y, or z). The index is just * the position of the number in that list. * * For example, if you have a vector with values 10, 20, 30 the index 0 would * give you the first value 10, index 1 would give you the second value 20, * and so on. * * @param {Number} index - The position of the value you want to get from the vector. * @returns {Number} The value at the specified position in the vector. * @throws Will throw an error if the index is out of bounds, meaning if you try to * get a value from a position that doesn't exist in the vector. */ getValue(index) { if (index < this._values.length) { return this._values[index]; } else { p5._friendlyError( 'The index parameter is trying to set a value outside the bounds of the vector', 'p5.Vector.setValue' ); } } /** * Sets the value at the specified index of the vector. * * This method allows you to change a specific component of the vector by providing its index and the new value you want to set. * Think of the vector as a list of numbers, where each number represents a different direction (like x, y, or z). * The index is just the position of the number in that list. * * For example, if you have a vector with values [0, 20, 30], and you want to change the second value (20) to 50, * you would use this method with index 1 (since indexes start at 0) and value 50. * * @param {Number} index - The position in the vector where you want to set the new value. * @param {Number} value - The new value you want to set at the specified position. * @throws Will throw an error if the index is outside the bounds of the vector, meaning if you try to set a value at a position that doesn't exist in the vector. */ setValue(index, value) { if (index < this._values.length) { this._values[index] = value; } else { p5._friendlyError( 'The index parameter is trying to set a value outside the bounds of the vector', 'p5.Vector.setValue' ); } } /** * Gets the y component of the vector. * * This method returns the value of the y component of the vector. * Think of the y component as the vertical position or the second number in the vector. * If the y component is not defined, it will return 0. * * @returns {Number} The y component of the vector. Returns 0 if the value is not defined. */ get y() { return this._values[1] || 0; } /** * Gets the z component of the vector. * * This method returns the value of the z component of the vector. * Think of the z component as the depth or the third number in the vector. * If the z component is not defined, it will return 0. * * @returns {Number} The z component of the vector. Returns 0 if the value is not defined. */ get z() { return this._values[2] || 0; } /** * Gets the w component of the vector. * * This method returns the value of the w component of the vector. * Think of the w component as the fourth number in the vector. * If the w component is not defined, it will return 0. * * @returns {Number} The w component of the vector. Returns 0 if the value is not defined. */ get w() { return this._values[3] || 0; } /** * Sets the x component of the vector. * * This method allows you to change the x value of the vector. * The x value is the first number in the vector, representing the horizontal position. * By calling this method, you can update the x value to a new number. * * @param {Number} xVal - The new value for the x component. */ set x(xVal) { if (this._values.length > 1) { this._values[0] = xVal; } } /** * Sets the y component of the vector. * * This method allows you to change the y value of the vector. * The y value is the second number in the vector, representing the vertical position. * By calling this method, you can update the y value to a new number. * * @param {Number} yVal - The new value for the y component. */ set y(yVal) { if (this._values.length > 1) { this._values[1] = yVal; } } /** * Sets the z component of the vector. * * This method allows you to change the z value of the vector. * The z value is the third number in the vector, representing the depth or the third dimension. * By calling this method, you can update the z value to a new number. * * @param {Number} zVal - The new value for the z component. */ set z(zVal) { if (this._values.length > 2) { this._values[2] = zVal; } } /** * Sets the w component of the vector. * * This method allows you to change the w value of the vector. * The w value is the fourth number in the vector, representing the fourth dimension. * By calling this method, you can update the w value to a new number. * * @param {Number} wVal - The new value for the w component. */ set w(wVal) { if (this._values.length > 3) { this._values[3] = wVal; } } /** * Returns a string representation of a vector. * * Calling `toString()` is useful for printing vectors to the console while * debugging. * * @return {String} string representation of the vector. * * @example * // META:norender * function setup() { * let v = createVector(20, 30); * * // Prints 'vector[20, 30, 0]'. * print(v.toString()); * } */ toString() { return `vector[${this._values.join(', ')}]`; } /** * Sets the vector's `x`, `y`, and `z` components. * * `set()` can use separate numbers, as in `v.set(1, 2, 3)`, a * p5.Vector object, as in `v.set(v2)`, or an * array of numbers, as in `v.set([1, 2, 3])`. * * If a value isn't provided for a component, it will be set to 0. For * example, `v.set(4, 5)` sets `v.x` to 4, `v.y` to 5, and `v.z` to 0. * Calling `set()` with no arguments, as in `v.set()`, sets all the vector's * components to 0. * * @param {Number} [x] x component of the vector. * @param {Number} [y] y component of the vector. * @param {Number} [z] z component of the vector. * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Top left. * let pos = createVector(25, 25); * point(pos); * * // Top right. * // set() with numbers. * pos.set(75, 25); * point(pos); * * // Bottom right. * // set() with a p5.Vector. * let p2 = createVector(75, 75); * pos.set(p2); * point(pos); * * // Bottom left. * // set() with an array. * let arr = [25, 75]; * pos.set(arr); * point(pos); * * describe('Four black dots arranged in a square on a gray background.'); * } */ /** * @param {p5.Vector|Number[]} value vector to set. * @chainable */ set(...args) { if (args[0] instanceof Vector) { this._values = args[0].values.slice(); } else if (Array.isArray(args[0])) { this._values = args[0].map(arg => arg || 0); } else { this._values = args.map(arg => arg || 0); } this.dimensions = this._values.length; return this; } /** * Returns a copy of the p5.Vector object. * * @return {p5.Vector} copy of the p5.Vector object. * * @example * function setup() { * createCanvas(100 ,100); * * background(200); * * // Create a p5.Vector object. * let pos = createVector(50, 50); * * // Make a copy. * let pc = pos.copy(); * * // Draw the point. * strokeWeight(5); * point(pc); * * describe('A black point drawn in the middle of a gray square.'); * } */ copy() { if (this.isPInst) { return new Vector(this._fromRadians, this._toRadians, ...this._values); } else { return new Vector(...this._values); } } /** * Adds to a vector's components. * * `add()` can use separate numbers, as in `v.add(1, 2, 3)`, * another p5.Vector object, as in `v.add(v2)`, or * an array of numbers, as in `v.add([1, 2, 3])`. * * If a value isn't provided for a component, it won't change. For * example, `v.add(4, 5)` adds 4 to `v.x`, 5 to `v.y`, and 0 to `v.z`. * Calling `add()` with no arguments, as in `v.add()`, has no effect. * * This method supports N-dimensional vectors. * * The static version of `add()`, as in `p5.Vector.add(v2, v1)`, returns a new * p5.Vector object and doesn't change the * originals. * * @param {Number|Array} x x component of the vector to be added or an array of components. * @param {Number} [y] y component of the vector to be added. * @param {Number} [z] z component of the vector to be added. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Top left. * let pos = createVector(25, 25); * point(pos); * * // Top right. * // Add numbers. * pos.add(50, 0); * point(pos); * * // Bottom right. * // Add a p5.Vector. * let p2 = createVector(0, 50); * pos.add(p2); * point(pos); * * // Bottom left. * // Add an array. * let arr = [-50, 0]; * pos.add(arr); * point(pos); * * describe('Four black dots arranged in a square on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Top left. * let p1 = createVector(25, 25); * * // Center. * let p2 = createVector(50, 50); * * // Bottom right. * // Add p1 and p2. * let p3 = p5.Vector.add(p1, p2); * * // Draw the points. * strokeWeight(5); * point(p1); * point(p2); * point(p3); * * describe('Three black dots in a diagonal line from top left to bottom right.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Three arrows drawn on a gray square. A red arrow extends from the top left corner to the center. A blue arrow extends from the tip of the red arrow. A purple arrow extends from the origin to the tip of the blue arrow.'); * } * * function draw() { * background(200); * * let origin = createVector(0, 0); * * // Draw the red arrow. * let v1 = createVector(50, 50); * drawArrow(origin, v1, 'red'); * * // Draw the blue arrow. * let v2 = createVector(-30, 20); * drawArrow(v1, v2, 'blue'); * * // Purple arrow. * let v3 = p5.Vector.add(v1, v2); * drawArrow(origin, v3, 'purple'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ /** * @param {p5.Vector|Number[]} value The vector to add * @chainable */ add(...args) { if (args[0] instanceof Vector) { args = args[0].values; } else if (Array.isArray(args[0])) { args = args[0]; } args.forEach((value, index) => { this._values[index] = (this._values[index] || 0) + (value || 0); }); return this; } /** * Performs modulo (remainder) division with a vector's `x`, `y`, and `z` * components. * * `rem()` can use separate numbers, as in `v.rem(1, 2, 3)`, * another p5.Vector object, as in `v.rem(v2)`, or * an array of numbers, as in `v.rem([1, 2, 3])`. * * If only one value is provided, as in `v.rem(2)`, then all the components * will be set to their values modulo 2. If two values are provided, as in * `v.rem(2, 3)`, then `v.z` won't change. Calling `rem()` with no * arguments, as in `v.rem()`, has no effect. * * The static version of `rem()`, as in `p5.Vector.rem(v2, v1)`, returns a * new p5.Vector object and doesn't change the * originals. * * @param {Number} x x component of divisor vector. * @param {Number} y y component of divisor vector. * @param {Number} z z component of divisor vector. * @chainable * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(3, 4, 5); * * // Divide numbers. * v.rem(2); * * // Prints 'p5.Vector Object : [1, 0, 1]'. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(3, 4, 5); * * // Divide numbers. * v.rem(2, 3); * * // Prints 'p5.Vector Object : [1, 1, 5]'. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(3, 4, 5); * * // Divide numbers. * v.rem(2, 3, 4); * * // Prints 'p5.Vector Object : [1, 1, 1]'. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v1 = createVector(3, 4, 5); * let v2 = createVector(2, 3, 4); * * // Divide a p5.Vector. * v1.rem(v2); * * // Prints 'p5.Vector Object : [1, 1, 1]'. * print(v1.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(3, 4, 5); * * // Divide an array. * let arr = [2, 3, 4]; * v.rem(arr); * * // Prints 'p5.Vector Object : [1, 1, 1]'. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v1 = createVector(3, 4, 5); * let v2 = createVector(2, 3, 4); * * // Divide without modifying the original vectors. * let v3 = p5.Vector.rem(v1, v2); * * // Prints 'p5.Vector Object : [1, 1, 1]'. * print(v3.toString()); * } */ /** * @param {p5.Vector | Number[]} value divisor vector. * @chainable */ rem(x, y, z) { if (x instanceof Vector) { if ([x.x, x.y, x.z].every(Number.isFinite)) { const xComponent = parseFloat(x.x); const yComponent = parseFloat(x.y); const zComponent = parseFloat(x.z); return calculateRemainder3D.call( this, xComponent, yComponent, zComponent ); } } else if (Array.isArray(x)) { if (x.every(element => Number.isFinite(element))) { if (x.length === 2) { return calculateRemainder2D.call(this, x[0], x[1]); } if (x.length === 3) { return calculateRemainder3D.call(this, x[0], x[1], x[2]); } } } else if (arguments.length === 1) { if (Number.isFinite(arguments[0]) && arguments[0] !== 0) { this.x = this.x % arguments[0]; this.y = this.y % arguments[0]; this.z = this.z % arguments[0]; return this; } } else if (arguments.length === 2) { const vectorComponents = [...arguments]; if (vectorComponents.every(element => Number.isFinite(element))) { if (vectorComponents.length === 2) { return calculateRemainder2D.call( this, vectorComponents[0], vectorComponents[1] ); } } } else if (arguments.length === 3) { const vectorComponents = [...arguments]; if (vectorComponents.every(element => Number.isFinite(element))) { if (vectorComponents.length === 3) { return calculateRemainder3D.call( this, vectorComponents[0], vectorComponents[1], vectorComponents[2] ); } } } } /** * Subtracts from a vector's `x`, `y`, and `z` components. * * `sub()` can use separate numbers, as in `v.sub(1, 2, 3)`, another * p5.Vector object, as in `v.sub(v2)`, or an array * of numbers, as in `v.sub([1, 2, 3])`. * * If a value isn't provided for a component, it won't change. For * example, `v.sub(4, 5)` subtracts 4 from `v.x`, 5 from `v.y`, and 0 from `v.z`. * Calling `sub()` with no arguments, as in `v.sub()`, has no effect. * * The static version of `sub()`, as in `p5.Vector.sub(v2, v1)`, returns a new * p5.Vector object and doesn't change the * originals. * * @param {Number} x x component of the vector to subtract. * @param {Number} [y] y component of the vector to subtract. * @param {Number} [z] z component of the vector to subtract. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Bottom right. * let pos = createVector(75, 75); * point(pos); * * // Top right. * // Subtract numbers. * pos.sub(0, 50); * point(pos); * * // Top left. * // Subtract a p5.Vector. * let p2 = createVector(50, 0); * pos.sub(p2); * point(pos); * * // Bottom left. * // Subtract an array. * let arr = [0, -50]; * pos.sub(arr); * point(pos); * * describe('Four black dots arranged in a square on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create p5.Vector objects. * let p1 = createVector(75, 75); * let p2 = createVector(50, 50); * * // Subtract with modifying the original vectors. * let p3 = p5.Vector.sub(p1, p2); * * // Draw the points. * strokeWeight(5); * point(p1); * point(p2); * point(p3); * * describe('Three black dots in a diagonal line from top left to bottom right.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow.'); * } * * function draw() { * background(200); * * let origin = createVector(0, 0); * * // Draw the red arrow. * let v1 = createVector(50, 50); * drawArrow(origin, v1, 'red'); * * // Draw the blue arrow. * let v2 = createVector(20, 70); * drawArrow(origin, v2, 'blue'); * * // Purple arrow. * let v3 = p5.Vector.sub(v2, v1); * drawArrow(v1, v3, 'purple'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ /** * @param {p5.Vector|Number[]} value the vector to subtract * @chainable */ sub(...args) { if (args[0] instanceof Vector) { args[0].values.forEach((value, index) => { this._values[index] -= value || 0; }); } else if (Array.isArray(args[0])) { args[0].forEach((value, index) => { this._values[index] -= value || 0; }); } else { args.forEach((value, index) => { this._values[index] -= value || 0; }); } return this; } /** * Multiplies a vector's `x`, `y`, and `z` components. * * `mult()` can use separate numbers, as in `v.mult(1, 2, 3)`, another * p5.Vector object, as in `v.mult(v2)`, or an array * of numbers, as in `v.mult([1, 2, 3])`. * * If only one value is provided, as in `v.mult(2)`, then all the components * will be multiplied by 2. If a value isn't provided for a component, it * won't change. For example, `v.mult(4, 5)` multiplies `v.x` by, `v.y` by 5, * and `v.z` by 1. Calling `mult()` with no arguments, as in `v.mult()`, has * no effect. * * The static version of `mult()`, as in `p5.Vector.mult(v, 2)`, returns a new * p5.Vector object and doesn't change the * originals. * * @param {Number} n The number to multiply with the vector * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Top-left. * let p = createVector(25, 25); * point(p); * * // Center. * // Multiply all components by 2. * p.mult(2); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.'); * } * * @example * function setup() { * strokeWeight(5); * * // Top-left. * let p = createVector(25, 25); * point(p); * * // Bottom-right. * // Multiply p.x * 2 and p.y * 3 * p.mult(2, 3); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Top-left. * let p = createVector(25, 25); * point(p); * * // Bottom-right. * // Multiply p.x * 2 and p.y * 3 * let arr = [2, 3]; * p.mult(arr); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Top-left. * let p = createVector(25, 25); * point(p); * * // Bottom-right. * // Multiply p.x * p2.x and p.y * p2.y * let p2 = createVector(2, 3); * p.mult(p2); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Top-left. * let p = createVector(25, 25); * point(p); * * // Bottom-right. * // Create a new p5.Vector with * // p3.x = p.x * p2.x * // p3.y = p.y * p2.y * let p2 = createVector(2, 3); * let p3 = p5.Vector.mult(p, p2); * point(p3); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Two arrows extending from the top left corner. The blue arrow is twice the length of the red arrow.'); * } * * function draw() { * background(200); * * let origin = createVector(0, 0); * * // Draw the red arrow. * let v1 = createVector(25, 25); * drawArrow(origin, v1, 'red'); * * // Draw the blue arrow. * let v2 = p5.Vector.mult(v1, 2); * drawArrow(origin, v2, 'blue'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ /** * @param {Number} x number to multiply with the x component of the vector. * @param {Number} y number to multiply with the y component of the vector. * @param {Number} [z] number to multiply with the z component of the vector. * @chainable */ /** * @param {Number[]} arr array to multiply with the components of the vector. * @chainable */ /** * @param {p5.Vector} v vector to multiply with the components of the original vector. * @chainable */ mult(...args) { if (args.length === 1 && args[0] instanceof Vector) { const v = args[0]; const maxLen = Math.min(this._values.length, v.values.length); for (let i = 0; i < maxLen; i++) { if (Number.isFinite(v.values[i]) && typeof v.values[i] === 'number') { this._values[i] *= v.values[i]; } else { console.warn( 'p5.Vector.prototype.mult:', 'v contains components that are either undefined or not finite numbers' ); return this; } } } else if (args.length === 1 && Array.isArray(args[0])) { const arr = args[0]; const maxLen = Math.min(this._values.length, arr.length); for (let i = 0; i < maxLen; i++) { if (Number.isFinite(arr[i]) && typeof arr[i] === 'number') { this._values[i] *= arr[i]; } else { console.warn( 'p5.Vector.prototype.mult:', 'arr contains elements that are either undefined or not finite numbers' ); return this; } } } else if ( args.length === 1 && typeof args[0] === 'number' && Number.isFinite(args[0]) ) { for (let i = 0; i < this._values.length; i++) { this._values[i] *= args[0]; } } return this; } /** * Divides a vector's `x`, `y`, and `z` components. * * `div()` can use separate numbers, as in `v.div(1, 2, 3)`, another * p5.Vector object, as in `v.div(v2)`, or an array * of numbers, as in `v.div([1, 2, 3])`. * * If only one value is provided, as in `v.div(2)`, then all the components * will be divided by 2. If a value isn't provided for a component, it * won't change. For example, `v.div(4, 5)` divides `v.x` by, `v.y` by 5, * and `v.z` by 1. Calling `div()` with no arguments, as in `v.div()`, has * no effect. * * The static version of `div()`, as in `p5.Vector.div(v, 2)`, returns a new * p5.Vector object and doesn't change the * originals. * * @param {Number} n The number to divide the vector by * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Center. * let p = createVector(50, 50); * point(p); * * // Top-left. * // Divide p.x / 2 and p.y / 2 * p.div(2); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Bottom-right. * let p = createVector(50, 75); * point(p); * * // Top-left. * // Divide p.x / 2 and p.y / 3 * p.div(2, 3); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Bottom-right. * let p = createVector(50, 75); * point(p); * * // Top-left. * // Divide p.x / 2 and p.y / 3 * let arr = [2, 3]; * p.div(arr); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Bottom-right. * let p = createVector(50, 75); * point(p); * * // Top-left. * // Divide p.x / 2 and p.y / 3 * let p2 = createVector(2, 3); * p.div(p2); * point(p); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the points. * strokeWeight(5); * * // Bottom-right. * let p = createVector(50, 75); * point(p); * * // Top-left. * // Create a new p5.Vector with * // p3.x = p.x / p2.x * // p3.y = p.y / p2.y * let p2 = createVector(2, 3); * let p3 = p5.Vector.div(p, p2); * point(p3); * * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); * } * * @example * function draw() { * background(200); * * let origin = createVector(0, 0); * * // Draw the red arrow. * let v1 = createVector(50, 50); * drawArrow(origin, v1, 'red'); * * // Draw the blue arrow. * let v2 = p5.Vector.div(v1, 2); * drawArrow(origin, v2, 'blue'); * * describe('Two arrows extending from the top left corner. The blue arrow is half the length of the red arrow.'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ /** * @param {Number} x number to divide with the x component of the vector. * @param {Number} y number to divide with the y component of the vector. * @param {Number} [z] number to divide with the z component of the vector. * @chainable */ /** * @param {Number[]} arr array to divide the components of the vector by. * @chainable */ /** * @param {p5.Vector} v vector to divide the components of the original vector by. * @chainable */ div(...args) { if (args.length === 0) return this; if (args.length === 1 && args[0] instanceof Vector) { const v = args[0]; if ( v._values.every( val => Number.isFinite(val) && typeof val === 'number' ) ) { if (v._values.some(val => val === 0)) { console.warn('p5.Vector.prototype.div:', 'divide by 0'); return this; } this._values = this._values.map((val, i) => val / v._values[i]); } else { console.warn( 'p5.Vector.prototype.div:', 'vector contains components that are either undefined or not finite numbers' ); } return this; } if (args.length === 1 && Array.isArray(args[0])) { const arr = args[0]; if (arr.every(val => Number.isFinite(val) && typeof val === 'number')) { if (arr.some(val => val === 0)) { console.warn('p5.Vector.prototype.div:', 'divide by 0'); return this; } this._values = this._values.map((val, i) => val / arr[i]); } else { console.warn( 'p5.Vector.prototype.div:', 'array contains components that are either undefined or not finite numbers' ); } return this; } if (args.every(val => Number.isFinite(val) && typeof val === 'number')) { if (args.some(val => val === 0)) { console.warn('p5.Vector.prototype.div:', 'divide by 0'); return this; } this._values = this._values.map((val, i) => val / args[0]); } else { console.warn( 'p5.Vector.prototype.div:', 'arguments contain components that are either undefined or not finite numbers' ); } return this; } /** * Calculates the magnitude (length) of the vector. * * Use mag() to calculate the magnitude of a 2D vector * using components as in `mag(x, y)`. * * @return {Number} magnitude of the vector. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Vector object. * let p = createVector(30, 40); * * // Draw a line from the origin. * line(0, 0, p.x, p.y); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the vector's magnitude. * let m = p.mag(); * text(m, p.x, p.y); * * describe('A diagonal black line extends from the top left corner of a gray square. The number 50 is written at the end of the line.'); * } */ mag() { return Math.sqrt(this.magSq()); } /** * Calculates the magnitude (length) of the vector squared. * * @return {Number} squared magnitude of the vector. * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Vector object. * let p = createVector(30, 40); * * // Draw a line from the origin. * line(0, 0, p.x, p.y); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the vector's magnitude squared. * let m = p.magSq(); * text(m, p.x, p.y); * * describe('A diagonal black line extends from the top left corner of a gray square. The number 2500 is written at the end of the line.'); * } */ magSq() { return this._values.reduce( (sum, component) => sum + component * component, 0 ); } /** * Calculates the dot product of two vectors. * * The dot product is a number that describes the overlap between two vectors. * Visually, the dot product can be thought of as the "shadow" one vector * casts on another. The dot product's magnitude is largest when two vectors * point in the same or opposite directions. Its magnitude is 0 when two * vectors form a right angle. * * The version of `dot()` with one parameter interprets it as another * p5.Vector object. * * The version of `dot()` with multiple parameters interprets them as the * `x`, `y`, and `z` components of another vector. * * The static version of `dot()`, as in `p5.Vector.dot(v1, v2)`, is the same * as calling `v1.dot(v2)`. * * @param {Number} x x component of the vector. * @param {Number} [y] y component of the vector. * @param {Number} [z] z component of the vector. * @return {Number} dot product. * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v1 = createVector(3, 4); * let v2 = createVector(3, 0); * * // Calculate the dot product. * let dp = v1.dot(v2); * * // Prints "9" to the console. * print(dp); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v1 = createVector(1, 0); * let v2 = createVector(0, 1); * * // Calculate the dot product. * let dp = p5.Vector.dot(v1, v2); * * // Prints "0" to the console. * print(dp); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Two arrows drawn on a gray square. A black arrow points to the right and a red arrow follows the mouse. The text "v1 • v2 = something" changes as the mouse moves.'); * } * * function draw() { * background(200); * * // Center. * let v0 = createVector(50, 50); * * // Draw the black arrow. * let v1 = createVector(30, 0); * drawArrow(v0, v1, 'black'); * * // Draw the red arrow. * let v2 = createVector(mouseX - 50, mouseY - 50); * drawArrow(v0, v2, 'red'); * * // Display the dot product. * let dp = v2.dot(v1); * text(`v2 • v1 = ${dp}`, 10, 20); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ /** * @param {p5.Vector} v p5.Vector to be dotted. * @return {Number} */ dot(...args) { if (args[0] instanceof Vector) { return this.dot(...args[0]._values); } return this._values.reduce((sum, component, index) => { return sum + component * (args[index] || 0); }, 0); } /** * Calculates the cross product of two vectors. * * The cross product is a vector that points straight out of the plane created * by two vectors. The cross product's magnitude is the area of the parallelogram * formed by the original two vectors. * * The static version of `cross()`, as in `p5.Vector.cross(v1, v2)`, is the same * as calling `v1.cross(v2)`. * * @param {p5.Vector} v p5.Vector to be crossed. * @return {p5.Vector} cross product as a p5.Vector. * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v1 = createVector(1, 0); * let v2 = createVector(3, 4); * * // Calculate the cross product. * let cp = v1.cross(v2); * * // Prints "p5.Vector Object : [0, 0, 4]" to the console. * print(cp.toString()); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v1 = createVector(1, 0); * let v2 = createVector(3, 4); * * // Calculate the cross product. * let cp = p5.Vector.cross(v1, v2); * * // Prints "p5.Vector Object : [0, 0, 4]" to the console. * print(cp.toString()); * } */ cross(v) { const x = this.y * v.z - this.z * v.y; const y = this.z * v.x - this.x * v.z; const z = this.x * v.y - this.y * v.x; if (this.isPInst) { return new Vector(this._fromRadians, this._toRadians, x, y, z); } else { return new Vector(x, y, z); } } /** * Calculates the distance between two points represented by vectors. * * A point's coordinates can be represented by the components of a vector * that extends from the origin to the point. * * The static version of `dist()`, as in `p5.Vector.dist(v1, v2)`, is the same * as calling `v1.dist(v2)`. * * Use dist() to calculate the distance between points * using coordinates as in `dist(x1, y1, x2, y2)`. * * @submodule p5.Vector * @param {p5.Vector} v x, y, and z coordinates of a p5.Vector. * @return {Number} distance. * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create p5.Vector objects. * let v1 = createVector(1, 0); * let v2 = createVector(0, 1); * * // Calculate the distance between them. * let d = v1.dist(v2); * * // Prints "1.414..." to the console. * print(d); * } * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create p5.Vector objects. * let v1 = createVector(1, 0); * let v2 = createVector(0, 1); * * // Calculate the distance between them. * let d = p5.Vector.dist(v1, v2); * * // Prints "1.414..." to the console. * print(d); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow. The number 36 is written in black near the purple arrow.'); * } * * function draw() { * background(200); * * let origin = createVector(0, 0); * * // Draw the red arrow. * let v1 = createVector(50, 50); * drawArrow(origin, v1, 'red'); * * // Draw the blue arrow. * let v2 = createVector(20, 70); * drawArrow(origin, v2, 'blue'); * * // Purple arrow. * let v3 = p5.Vector.sub(v2, v1); * drawArrow(v1, v3, 'purple'); * * // Style the text. * textAlign(CENTER); * * // Display the magnitude. The same as floor(v3.mag()); * let m = floor(p5.Vector.dist(v1, v2)); * text(m, 50, 75); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ dist(v) { return v.copy().sub(this).mag(); } /** * Scales the components of a p5.Vector object so * that its magnitude is 1. * * The static version of `normalize()`, as in `p5.Vector.normalize(v)`, * returns a new p5.Vector object and doesn't change * the original. * * @return {p5.Vector} normalized p5.Vector. * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Vector. * let v = createVector(10, 20, 2); * * // Normalize. * v.normalize(); * * // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Vector. * let v0 = createVector(10, 20, 2); * * // Create a normalized copy. * let v1 = p5.Vector.normalize(v0); * * // Prints "p5.Vector Object : [10, 20, 2]" to the console. * print(v0.toString()); * // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console. * print(v1.toString()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow's length is fixed to the circle's radius."); * } * * function draw() { * background(240); * * // Vector to the center. * let v0 = createVector(50, 50); * * // Vector from the center to the mouse. * let v1 = createVector(mouseX - 50, mouseY - 50); * * // Circle's radius. * let r = 25; * * // Draw the red arrow. * drawArrow(v0, v1, 'red'); * * // Draw the blue arrow. * v1.normalize(); * drawArrow(v0, v1.mult(r), 'blue'); * * // Draw the circle. * noFill(); * circle(50, 50, r * 2); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ normalize() { const len = this.mag(); // here we multiply by the reciprocal instead of calling 'div()' // since div duplicates this zero check. if (len !== 0) this.mult(1 / len); return this; } /** * Limits a vector's magnitude to a maximum value. * * The static version of `limit()`, as in `p5.Vector.limit(v, 5)`, returns a * new p5.Vector object and doesn't change the * original. * * @param {Number} max maximum magnitude for the vector. * @chainable * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(10, 20, 2); * * // Limit its magnitude. * v.limit(5); * * // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v0 = createVector(10, 20, 2); * * // Create a copy an limit its magintude. * let v1 = p5.Vector.limit(v0, 5); * * // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console. * print(v1.toString()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow never crosses the circle's edge."); * } * function draw() { * background(240); * * // Vector to the center. * let v0 = createVector(50, 50); * * // Vector from the center to the mouse. * let v1 = createVector(mouseX - 50, mouseY - 50); * * // Circle's radius. * let r = 25; * * // Draw the red arrow. * drawArrow(v0, v1, 'red'); * * // Draw the blue arrow. * drawArrow(v0, v1.limit(r), 'blue'); * * // Draw the circle. * noFill(); * circle(50, 50, r * 2); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ limit(max) { const mSq = this.magSq(); if (mSq > max * max) { this.div(Math.sqrt(mSq)) //normalize it .mult(max); } return this; } /** * Sets a vector's magnitude to a given value. * * The static version of `setMag()`, as in `p5.Vector.setMag(v, 10)`, returns * a new p5.Vector object and doesn't change the * original. * * @param {Number} len new length for this vector. * @chainable * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(3, 4, 0); * * // Prints "5" to the console. * print(v.mag()); * * // Set its magnitude to 10. * v.setMag(10); * * // Prints "p5.Vector Object : [6, 8, 0]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v0 = createVector(3, 4, 0); * * // Create a copy with a magnitude of 10. * let v1 = p5.Vector.setMag(v0, 10); * * // Prints "5" to the console. * print(v0.mag()); * * // Prints "p5.Vector Object : [6, 8, 0]" to the console. * print(v1.toString()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Two arrows extend from the top left corner of a square toward its center. The red arrow reaches the center and the blue arrow only extends part of the way.'); * } * * function draw() { * background(240); * * let origin = createVector(0, 0); * let v = createVector(50, 50); * * // Draw the red arrow. * drawArrow(origin, v, 'red'); * * // Set v's magnitude to 30. * v.setMag(30); * * // Draw the blue arrow. * drawArrow(origin, v, 'blue'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ setMag(n) { return this.normalize().mult(n); } /** * Calculates the angle a 2D vector makes with the positive x-axis. * * By convention, the positive x-axis has an angle of 0. Angles increase in * the clockwise direction. * * If the vector was created with * createVector(), `heading()` returns angles * in the units of the current angleMode(). * * The static version of `heading()`, as in `p5.Vector.heading(v)`, works the * same way. * * @return {Number} angle of rotation. * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(1, 1); * * // Prints "0.785..." to the console. * print(v.heading()); * * // Use degrees. * angleMode(DEGREES); * * // Prints "45" to the console. * print(v.heading()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(1, 1); * * // Prints "0.785..." to the console. * print(p5.Vector.heading(v)); * * // Use degrees. * angleMode(DEGREES); * * // Prints "45" to the console. * print(p5.Vector.heading(v)); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A black arrow extends from the top left of a square to its center. The text "Radians: 0.79" and "Degrees: 45" is written near the tip of the arrow.'); * } * * function draw() { * background(200); * * let origin = createVector(0, 0); * let v = createVector(50, 50); * * // Draw the black arrow. * drawArrow(origin, v, 'black'); * * // Use radians. * angleMode(RADIANS); * * // Display the heading in radians. * let h = round(v.heading(), 2); * text(`Radians: ${h}`, 20, 70); * * // Use degrees. * angleMode(DEGREES); * * // Display the heading in degrees. * h = v.heading(); * text(`Degrees: ${h}`, 20, 85); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ heading() { const h = Math.atan2(this.y, this.x); if (this.isPInst) return this._fromRadians(h); return h; } /** * Rotates a 2D vector to a specific angle without changing its magnitude. * * By convention, the positive x-axis has an angle of 0. Angles increase in * the clockwise direction. * * If the vector was created with * createVector(), `setHeading()` uses * the units of the current angleMode(). * * @param {Number} angle angle of rotation. * @chainable * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(0, 1); * * // Prints "1.570..." to the console. * print(v.heading()); * * // Point to the left. * v.setHeading(PI); * * // Prints "3.141..." to the console. * print(v.heading()); * } * * @example * // META:norender * function setup() { * // Use degrees. * angleMode(DEGREES); * * // Create a p5.Vector object. * let v = createVector(0, 1); * * // Prints "90" to the console. * print(v.heading()); * * // Point to the left. * v.setHeading(180); * * // Prints "180" to the console. * print(v.heading()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Two arrows extend from the center of a gray square. The red arrow points to the right and the blue arrow points down.'); * } * * function draw() { * background(200); * * // Create p5.Vector objects. * let v0 = createVector(50, 50); * let v1 = createVector(30, 0); * * // Draw the red arrow. * drawArrow(v0, v1, 'red'); * * // Point down. * v1.setHeading(HALF_PI); * * // Draw the blue arrow. * drawArrow(v0, v1, 'blue'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ 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); return this; } /** * Rotates a 2D vector by an angle without changing its magnitude. * * By convention, the positive x-axis has an angle of 0. Angles increase in * the clockwise direction. * * If the vector was created with * createVector(), `rotate()` uses * the units of the current angleMode(). * * The static version of `rotate()`, as in `p5.Vector.rotate(v, PI)`, * returns a new p5.Vector object and doesn't change * the original. * * @param {Number} angle angle of rotation. * @chainable * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(1, 0); * * // Prints "p5.Vector Object : [1, 0, 0]" to the console. * print(v.toString()); * * // Rotate a quarter turn. * v.rotate(HALF_PI); * * // Prints "p5.Vector Object : [0, 1, 0]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Use degrees. * angleMode(DEGREES); * * // Create a p5.Vector object. * let v = createVector(1, 0); * * // Prints "p5.Vector Object : [1, 0, 0]" to the console. * print(v.toString()); * * // Rotate a quarter turn. * v.rotate(90); * * // Prints "p5.Vector Object : [0, 1, 0]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v0 = createVector(1, 0); * * // Create a rotated copy. * let v1 = p5.Vector.rotate(v0, HALF_PI); * * // Prints "p5.Vector Object : [1, 0, 0]" to the console. * print(v0.toString()); * // Prints "p5.Vector Object : [0, 1, 0]" to the console. * print(v1.toString()); * } * * @example * // META:norender * function setup() { * // Use degrees. * angleMode(DEGREES); * * // Create a p5.Vector object. * let v0 = createVector(1, 0); * * // Create a rotated copy. * let v1 = p5.Vector.rotate(v0, 90); * * // Prints "p5.Vector Object : [1, 0, 0]" to the console. * print(v0.toString()); * * // Prints "p5.Vector Object : [0, 1, 0]" to the console. * print(v1.toString()); * } * * @example * let v0; * let v1; * * function setup() { * createCanvas(100, 100); * * // Create p5.Vector objects. * v0 = createVector(50, 50); * v1 = createVector(30, 0); * * describe('A black arrow extends from the center of a gray square. The arrow rotates clockwise.'); * } * * function draw() { * background(240); * * // Rotate v1. * v1.rotate(0.01); * * // Draw the black arrow. * drawArrow(v0, v1, 'black'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ rotate(a) { let newHeading = this.heading() + a; if (this.isPInst) newHeading = this._toRadians(newHeading); const mag = this.mag(); this.x = Math.cos(newHeading) * mag; this.y = Math.sin(newHeading) * mag; return this; } /** * Calculates the angle between two vectors. * * The angles returned are signed, which means that * `v1.angleBetween(v2) === -v2.angleBetween(v1)`. * * If the vector was created with * createVector(), `angleBetween()` returns * angles in the units of the current * angleMode(). * * @param {p5.Vector} value x, y, and z components of a p5.Vector. * @return {Number} angle between the vectors. * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v0 = createVector(1, 0); * let v1 = createVector(0, 1); * * // Prints "1.570..." to the console. * print(v0.angleBetween(v1)); * * // Prints "-1.570..." to the console. * print(v1.angleBetween(v0)); * } * * @example * // META:norender * function setup() { * // Use degrees. * angleMode(DEGREES); * // Create p5.Vector objects. * let v0 = createVector(1, 0); * let v1 = createVector(0, 1); * * // Prints "90" to the console. * print(v0.angleBetween(v1)); * * // Prints "-90" to the console. * print(v1.angleBetween(v0)); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v0 = createVector(1, 0); * let v1 = createVector(0, 1); * * // Prints "1.570..." to the console. * print(p5.Vector.angleBetween(v0, v1)); * * // Prints "-1.570..." to the console. * print(p5.Vector.angleBetween(v1, v0)); * } * * @example * // META:norender * function setup() { * // Use degrees. * angleMode(DEGREES); * * // Create p5.Vector objects. * let v0 = createVector(1, 0); * let v1 = createVector(0, 1); * * // Prints "90" to the console. * print(p5.Vector.angleBetween(v0, v1)); * * // Prints "-90" to the console. * print(p5.Vector.angleBetween(v1, v0)); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Two arrows extend from the center of a gray square. A red arrow points to the right and a blue arrow points down. The text "Radians: 1.57" and "Degrees: 90" is written above the arrows.'); * } * function draw() { * background(200); * * // Create p5.Vector objects. * let v0 = createVector(50, 50); * let v1 = createVector(30, 0); * let v2 = createVector(0, 30); * * // Draw the red arrow. * drawArrow(v0, v1, 'red'); * * // Draw the blue arrow. * drawArrow(v0, v2, 'blue'); * * // Use radians. * angleMode(RADIANS); * * // Display the angle in radians. * let angle = round(v1.angleBetween(v2), 2); * text(`Radians: ${angle}`, 20, 20); * * // Use degrees. * angleMode(DEGREES); * * // Display the angle in degrees. * angle = round(v1.angleBetween(v2), 2); * text(`Degrees: ${angle}`, 20, 35); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ angleBetween(v) { const magSqMult = this.magSq() * v.magSq(); // Returns NaN if either vector is the zero vector. if (magSqMult === 0) { return NaN; } const u = this.cross(v); // The dot product computes the cos value, and the cross product computes // the sin value. Find the angle based on them. In addition, in the case of // 2D vectors, a sign is added according to the direction of the vector. let angle = Math.atan2(u.mag(), this.dot(v)) * Math.sign(u.z || 1); if (this.isPInst) { angle = this._fromRadians(angle); } return angle; } /** * Calculates new `x`, `y`, and `z` components that are proportionally the * same distance between two vectors. * * The `amt` parameter is the amount to interpolate between the old vector and * the new vector. 0.0 keeps all components equal to the old vector's, 0.5 is * halfway between, and 1.0 sets all components equal to the new vector's. * * The static version of `lerp()`, as in `p5.Vector.lerp(v0, v1, 0.5)`, * returns a new p5.Vector object and doesn't change * the original. * * @param {Number} x x component. * @param {Number} y y component. * @param {Number} z z component. * @param {Number} amt amount of interpolation between 0.0 (old vector) * and 1.0 (new vector). 0.5 is halfway between. * @chainable * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v0 = createVector(1, 1, 1); * let v1 = createVector(3, 3, 3); * * // Interpolate. * v0.lerp(v1, 0.5); * * // Prints "p5.Vector Object : [2, 2, 2]" to the console. * print(v0.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(1, 1, 1); * * // Interpolate. * v.lerp(3, 3, 3, 0.5); * * // Prints "p5.Vector Object : [2, 2, 2]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v0 = createVector(1, 1, 1); * let v1 = createVector(3, 3, 3); * * // Interpolate. * let v2 = p5.Vector.lerp(v0, v1, 0.5); * * // Prints "p5.Vector Object : [2, 2, 2]" to the console. * print(v2.toString()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points down, and a purple arrow points to the bottom right.'); * } * function draw() { * background(200); * * // Create p5.Vector objects. * let v0 = createVector(50, 50); * let v1 = createVector(30, 0); * let v2 = createVector(0, 30); * * // Interpolate. * let v3 = p5.Vector.lerp(v1, v2, 0.5); * * // Draw the red arrow. * drawArrow(v0, v1, 'red'); * * // Draw the blue arrow. * drawArrow(v0, v2, 'blue'); * * // Draw the purple arrow. * drawArrow(v0, v3, 'purple'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ /** * @param {p5.Vector} v p5.Vector to lerp toward. * @param {Number} amt * @chainable */ lerp(x, y, z, amt) { if (x instanceof Vector) { return this.lerp(x.x, x.y, x.z, y); } this.x += (x - this.x) * amt || 0; this.y += (y - this.y) * amt || 0; this.z += (z - this.z) * amt || 0; return this; } /** * Calculates a new heading and magnitude that are between two vectors. * * The `amt` parameter is the amount to interpolate between the old vector and * the new vector. 0.0 keeps the heading and magnitude equal to the old * vector's, 0.5 sets them halfway between, and 1.0 sets the heading and * magnitude equal to the new vector's. * * `slerp()` differs from lerp() because * it interpolates magnitude. Calling `v0.slerp(v1, 0.5)` sets `v0`'s * magnitude to a value halfway between its original magnitude and `v1`'s. * Calling `v0.lerp(v1, 0.5)` makes no such guarantee. * * The static version of `slerp()`, as in `p5.Vector.slerp(v0, v1, 0.5)`, * returns a new p5.Vector object and doesn't change * the original. * * @param {p5.Vector} v p5.Vector to slerp toward. * @param {Number} amt amount of interpolation between 0.0 (old vector) * and 1.0 (new vector). 0.5 is halfway between. * @return {p5.Vector} * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v0 = createVector(3, 0); * * // Prints "3" to the console. * print(v0.mag()); * * // Prints "0" to the console. * print(v0.heading()); * * // Create a p5.Vector object. * let v1 = createVector(0, 1); * * // Prints "1" to the console. * print(v1.mag()); * * // Prints "1.570..." to the console. * print(v1.heading()); * * // Interpolate halfway between v0 and v1. * v0.slerp(v1, 0.5); * * // Prints "2" to the console. * print(v0.mag()); * * // Prints "0.785..." to the console. * print(v0.heading()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v0 = createVector(3, 0); * * // Prints "3" to the console. * print(v0.mag()); * * // Prints "0" to the console. * print(v0.heading()); * * // Create a p5.Vector object. * let v1 = createVector(0, 1); * * // Prints "1" to the console. * print(v1.mag()); * * // Prints "1.570..." to the console. * print(v1.heading()); * * // Create a p5.Vector that's halfway between v0 and v1. * let v3 = p5.Vector.slerp(v0, v1, 0.5); * * // Prints "2" to the console. * print(v3.mag()); * * // Prints "0.785..." to the console. * print(v3.heading()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points to the left, and a purple arrow points down.'); * } * * function draw() { * background(200); * * // Create p5.Vector objects. * let v0 = createVector(50, 50); * let v1 = createVector(20, 0); * let v2 = createVector(-40, 0); * * // Create a p5.Vector that's halfway between v1 and v2. * let v3 = p5.Vector.slerp(v1, v2, 0.5); * * // Draw the red arrow. * drawArrow(v0, v1, 'red'); * * // Draw the blue arrow. * drawArrow(v0, v2, 'blue'); * * // Draw the purple arrow. * drawArrow(v0, v3, 'purple'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ slerp(v, amt) { // edge cases. if (amt === 0) { return this; } if (amt === 1) { return this.set(v); } // calculate magnitudes const selfMag = this.mag(); const vMag = v.mag(); const magmag = selfMag * vMag; // if either is a zero vector, linearly interpolate by these vectors if (magmag === 0) { this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); return this; } // the cross product of 'this' and 'v' is the axis of rotation const axis = this.cross(v); const axisMag = axis.mag(); // Calculates the angle between 'this' and 'v' const theta = Math.atan2(axisMag, this.dot(v)); // However, if the norm of axis is 0, normalization cannot be performed, // so we will divide the cases if (axisMag > 0) { axis.x /= axisMag; axis.y /= axisMag; axis.z /= axisMag; } else if (theta < Math.PI * 0.5) { // if the norm is 0 and the angle is less than PI/2, // the angle is very close to 0, so do linear interpolation. this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); return this; } else { // If the norm is 0 and the angle is more than PI/2, the angle is // very close to PI. // In this case v can be regarded as '-this', so take any vector // that is orthogonal to 'this' and use that as the axis. if (this.z === 0 && v.z === 0) { // if both this and v are 2D vectors, use (0,0,1) // this makes the result also a 2D vector. axis.set(0, 0, 1); } else if (this.x !== 0) { // if the x components is not 0, use (y, -x, 0) axis.set(this.y, -this.x, 0).normalize(); } else { // if the x components is 0, use (1,0,0) axis.set(1, 0, 0); } } // Since 'axis' is a unit vector, ey is a vector of the same length as 'this'. const ey = axis.cross(this); // interpolate the length with 'this' and 'v'. const lerpedMagFactor = 1 - amt + (amt * vMag) / selfMag; // imagine a situation where 'axis', 'this', and 'ey' are pointing // along the z, x, and y axes, respectively. // rotates 'this' around 'axis' by amt * theta towards 'ey'. const cosMultiplier = lerpedMagFactor * Math.cos(amt * theta); const sinMultiplier = lerpedMagFactor * Math.sin(amt * theta); // then, calculate 'result'. this.x = this.x * cosMultiplier + ey.x * sinMultiplier; this.y = this.y * cosMultiplier + ey.y * sinMultiplier; this.z = this.z * cosMultiplier + ey.z * sinMultiplier; return this; } /** * Reflects a vector about a line in 2D or a plane in 3D. * * The orientation of the line or plane is described by a normal vector that * points away from the shape. * * The static version of `reflect()`, as in `p5.Vector.reflect(v, n)`, * returns a new p5.Vector object and doesn't change * the original. * * @param {p5.Vector} surfaceNormal p5.Vector * to reflect about. * @chainable * @example * // META:norender * function setup() { * // Create a normal vector. * let n = createVector(0, 1); * // Create a vector to reflect. * let v = createVector(4, 6); * * // Reflect v about n. * v.reflect(n); * * // Prints "p5.Vector Object : [4, -6, 0]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create a normal vector. * let n = createVector(0, 1); * * // Create a vector to reflect. * let v0 = createVector(4, 6); * * // Create a reflected vector. * let v1 = p5.Vector.reflect(v0, n); * * // Prints "p5.Vector Object : [4, -6, 0]" to the console. * print(v1.toString()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('Three arrows extend from the center of a gray square with a vertical line down its middle. A black arrow points to the right, a blue arrow points to the bottom left, and a red arrow points to the bottom right.'); * } * function draw() { * background(200); * * // Draw a vertical line. * line(50, 0, 50, 100); * * // Create a normal vector. * let n = createVector(1, 0); * * // Center. * let v0 = createVector(50, 50); * * // Create a vector to reflect. * let v1 = createVector(30, 40); * * // Create a reflected vector. * let v2 = p5.Vector.reflect(v1, n); * * // Scale the normal vector for drawing. * n.setMag(30); * * // Draw the black arrow. * drawArrow(v0, n, 'black'); * * // Draw the red arrow. * drawArrow(v0, v1, 'red'); * * // Draw the blue arrow. * drawArrow(v0, v2, 'blue'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ reflect(surfaceNormal) { const surfaceNormalCopy = Vector.normalize(surfaceNormal); return this.sub(surfaceNormalCopy.mult(2 * this.dot(surfaceNormalCopy))); } /** * Returns the vector's components as an array of numbers. * * @return {Number[]} array with the vector's components. * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(20, 30); * * // Prints "[20, 30, 0]" to the console. * print(v.array()); * } */ array() { return [this.x || 0, this.y || 0, this.z || 0]; } /** * Checks whether all the vector's components are equal to another vector's. * * `equals()` returns `true` if the vector's components are all the same as another * vector's and `false` if not. * * The version of `equals()` with one parameter interprets it as another * p5.Vector object. * * The version of `equals()` with multiple parameters interprets them as the * components of another vector. Any missing parameters are assigned the value * 0. * * The static version of `equals()`, as in `p5.Vector.equals(v0, v1)`, * interprets both parameters as p5.Vector objects. * * @param {Number} [x] x component of the vector. * @param {Number} [y] y component of the vector. * @param {Number} [z] z component of the vector. * @return {Boolean} whether the vectors are equal. * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v0 = createVector(10, 20, 30); * let v1 = createVector(10, 20, 30); * let v2 = createVector(0, 0, 0); * * // Prints "true" to the console. * print(v0.equals(v1)); * * // Prints "false" to the console. * print(v0.equals(v2)); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v0 = createVector(5, 10, 20); * let v1 = createVector(5, 10, 20); * let v2 = createVector(13, 10, 19); * * // Prints "true" to the console. * print(v0.equals(v1.x, v1.y, v1.z)); * * // Prints "false" to the console. * print(v0.equals(v2.x, v2.y, v2.z)); * } * * @example * // META:norender * function setup() { * // Create p5.Vector objects. * let v0 = createVector(10, 20, 30); * let v1 = createVector(10, 20, 30); * let v2 = createVector(0, 0, 0); * * // Prints "true" to the console. * print(p5.Vector.equals(v0, v1)); * * // Prints "false" to the console. * print(p5.Vector.equals(v0, v2)); * } */ /** * @param {p5.Vector|Array} value vector to compare. * @return {Boolean} */ equals(...args) { let values; if (args[0] instanceof Vector) { values = args[0]._values; } else if (Array.isArray(args[0])) { values = args[0]; } else { values = args; } for (let i = 0; i < this._values.length; i++) { if (this._values[i] !== (values[i] || 0)) { return false; } } return true; } /** * Replaces the components of a p5.Vector that are very close to zero with zero. * * In computers, handling numbers with decimals can give slightly imprecise answers due to the way those numbers are represented. * This can make it hard to check if a number is zero, as it may be close but not exactly zero. * This method rounds very close numbers to zero to make those checks easier * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON * * @return {p5.Vector} with components very close to zero replaced with zero. * @chainable */ clampToZero() { for (let i = 0; i < this._values.length; i++) { this._values[i] = this._clampToZero(this._values[i]); } return this; } /** * Helper function for clampToZero * @private */ _clampToZero(val) { return Math.abs((val || 0) - 0) <= Number.EPSILON ? 0 : val; } // Static Methods /** * Creates a new 2D vector from an angle. * * @static * @param {Number} angle desired angle, in radians. Unaffected by angleMode(). * @param {Number} [length] length of the new vector (defaults to 1). * @return {p5.Vector} new p5.Vector object. * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = p5.Vector.fromAngle(0); * * // Prints "p5.Vector Object : [1, 0, 0]" to the console. * print(v.toString()); * } * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = p5.Vector.fromAngle(0, 30); * * // Prints "p5.Vector Object : [30, 0, 0]" to the console. * print(v.toString()); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A black arrow extends from the center of a gray square. It points to the right.'); * } * function draw() { * background(200); * * // Create a p5.Vector to the center. * let v0 = createVector(50, 50); * * // Create a p5.Vector with an angle 0 and magnitude 30. * let v1 = p5.Vector.fromAngle(0, 30); * * // Draw the black arrow. * drawArrow(v0, v1, 'black'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ static fromAngle(angle, length) { if (typeof length === 'undefined') { length = 1; } return new Vector(length * Math.cos(angle), length * Math.sin(angle)); } /** * Creates a new 3D vector from a pair of ISO spherical angles. * * @static * @param {Number} theta polar angle in radians (zero is up). * @param {Number} phi azimuthal angle in radians * (zero is out of the screen). * @param {Number} [length] length of the new vector (defaults to 1). * @return {p5.Vector} new p5.Vector object. * * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = p5.Vector.fromAngles(0, 0); * * // Prints "p5.Vector Object : [0, -1, 0]" to the console. * print(v.toString()); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A light shines on a pink sphere as it orbits.'); * } * * function draw() { * background(0); * * // Calculate the ISO angles. * let theta = frameCount * 0.05; * let phi = 0; * * // Create a p5.Vector object. * let v = p5.Vector.fromAngles(theta, phi, 100); * * // Create a point light using the p5.Vector. * let c = color('deeppink'); * pointLight(c, v); * * // Style the sphere. * fill(255); * noStroke(); * * // Draw the sphere. * sphere(35); * } */ static fromAngles(theta, phi, length) { if (typeof length === 'undefined') { length = 1; } const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); return new Vector( length * sinTheta * sinPhi, -length * cosTheta, length * sinTheta * cosPhi ); } /** * Creates a new 2D unit vector with a random heading. * * @static * @return {p5.Vector} new p5.Vector object. * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = p5.Vector.random2D(); * * // Prints "p5.Vector Object : [x, y]" to the console * // where x and y are small random numbers. * print(v.toString()); * } * * @example * function setup() { * createCanvas(100, 100); * * // Slow the frame rate. * frameRate(1); * * describe('A black arrow in extends from the center of a gray square. It changes direction once per second.'); * } * * function draw() { * background(200); * * // Create a p5.Vector to the center. * let v0 = createVector(50, 50); * * // Create a random p5.Vector. * let v1 = p5.Vector.random2D(); * * // Scale v1 for drawing. * v1.mult(30); * * // Draw the black arrow. * drawArrow(v0, v1, 'black'); * } * * // Draws an arrow between two vectors. * function drawArrow(base, vec, myColor) { * push(); * stroke(myColor); * strokeWeight(3); * fill(myColor); * translate(base.x, base.y); * line(0, 0, vec.x, vec.y); * rotate(vec.heading()); * let arrowSize = 7; * translate(vec.mag() - arrowSize, 0); * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); * pop(); * } */ static random2D() { return this.fromAngle(Math.random() * TWO_PI); } /** * Creates a new 3D unit vector with a random heading. * * @static * @return {p5.Vector} new p5.Vector object. * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = p5.Vector.random3D(); * * // Prints "p5.Vector Object : [x, y, z]" to the console * // where x, y, and z are small random numbers. * print(v.toString()); * } */ static random3D() { const angle = Math.random() * TWO_PI; const vz = Math.random() * 2 - 1; const vzBase = Math.sqrt(1 - vz * vz); const vx = vzBase * Math.cos(angle); const vy = vzBase * Math.sin(angle); return new Vector(vx, vy, vz); } // Returns a copy of a vector. /** * @static * @param {p5.Vector} v the p5.Vector to create a copy of * @return {p5.Vector} the copy of the p5.Vector object */ static copy(v) { return v.copy(v); } // Adds two vectors together and returns a new one. /** * @static * @param {p5.Vector} v1 A p5.Vector to add * @param {p5.Vector} v2 A p5.Vector to add * @param {p5.Vector} [target] vector to receive the result. * @return {p5.Vector} resulting p5.Vector. */ static add(v1, v2, target) { if (!target) { target = v1.copy(); if (arguments.length === 3) { p5._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.add' ); } } else { target.set(v1); } target.add(v2); return target; } // Returns a vector remainder when it is divided by another vector /** * @static * @param {p5.Vector} v1 The dividend p5.Vector * @param {p5.Vector} v2 The divisor p5.Vector */ /** * @static * @param {p5.Vector} v1 * @param {p5.Vector} v2 * @return {p5.Vector} The resulting p5.Vector */ static rem(v1, v2) { if (v1 instanceof Vector && v2 instanceof Vector) { let target = v1.copy(); target.rem(v2); return target; } } /* * Subtracts one p5.Vector from another and returns a new one. The second * vector (`v2`) is subtracted from the first (`v1`), resulting in `v1-v2`. */ /** * @static * @param {p5.Vector} v1 A p5.Vector to subtract from * @param {p5.Vector} v2 A p5.Vector to subtract * @param {p5.Vector} [target] vector to receive the result. * @return {p5.Vector} The resulting p5.Vector */ static sub(v1, v2, target) { if (!target) { target = v1.copy(); if (arguments.length === 3) { p5._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.sub' ); } } else { target.set(v1); } target.sub(v2); return target; } /** * Multiplies a vector by a scalar and returns a new vector. */ /** * @static * @param {Number} x * @param {Number} y * @param {Number} [z] * @return {p5.Vector} resulting new p5.Vector. */ /** * @static * @param {p5.Vector} v * @param {Number} n * @param {p5.Vector} [target] vector to receive the result. */ /** * @static * @param {p5.Vector} v0 * @param {p5.Vector} v1 * @param {p5.Vector} [target] */ /** * @static * @param {p5.Vector} v0 * @param {Number[]} arr * @param {p5.Vector} [target] */ static mult(v, n, target) { if (!target) { target = v.copy(); if (arguments.length === 3) { p5._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.mult' ); } } else { target.set(v); } target.mult(n); return target; } /** * Rotates the vector (only 2D vectors) by the given angle; magnitude remains the same. Returns a new vector. */ /** * @static * @param {p5.Vector} v * @param {Number} angle * @param {p5.Vector} [target] The vector to receive the result */ static rotate(v, a, target) { if (arguments.length === 2) { target = v.copy(); } else { if (!(target instanceof Vector)) { p5._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.rotate' ); } target.set(v); } target.rotate(a); return target; } /** * Divides a vector by a scalar and returns a new vector. */ /** * @static * @param {Number} x * @param {Number} y * @param {Number} [z] * @return {p5.Vector} The resulting new p5.Vector */ /** * @static * @param {p5.Vector} v * @param {Number} n * @param {p5.Vector} [target] The vector to receive the result */ /** * @static * @param {p5.Vector} v0 * @param {p5.Vector} v1 * @param {p5.Vector} [target] */ /** * @static * @param {p5.Vector} v0 * @param {Number[]} arr * @param {p5.Vector} [target] */ static div(v, n, target) { if (!target) { target = v.copy(); if (arguments.length === 3) { p5._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.div' ); } } else { target.set(v); } target.div(n); return target; } /** * Calculates the dot product of two vectors. */ /** * @static * @param {p5.Vector} v1 first p5.Vector. * @param {p5.Vector} v2 second p5.Vector. * @return {Number} dot product. */ static dot(v1, v2) { return v1.dot(v2); } /** * Calculates the cross product of two vectors. */ /** * @static * @param {p5.Vector} v1 first p5.Vector. * @param {p5.Vector} v2 second p5.Vector. * @return {p5.Vector} cross product. */ static cross(v1, v2) { return v1.cross(v2); } /** * Calculates the Euclidean distance between two points (considering a * point as a vector object). */ /** * @static * @param {p5.Vector} v1 The first p5.Vector * @param {p5.Vector} v2 The second p5.Vector * @return {Number} The distance */ static dist(v1, v2) { return v1.dist(v2); } /** * Linear interpolate a vector to another vector and return the result as a * new vector. */ /** * @static * @param {p5.Vector} v1 * @param {p5.Vector} v2 * @param {Number} amt * @param {p5.Vector} [target] The vector to receive the result * @return {p5.Vector} The lerped value */ static lerp(v1, v2, amt, target) { if (!target) { target = v1.copy(); if (arguments.length === 4) { p5._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.lerp' ); } } else { target.set(v1); } target.lerp(v2, amt); return target; } /** * Performs spherical linear interpolation with the other vector * and returns the resulting vector. * This works in both 3D and 2D. As for 2D, the result of slerping * between 2D vectors is always a 2D vector. */ /** * @static * @param {p5.Vector} v1 old vector. * @param {p5.Vector} v2 new vector. * @param {Number} amt * @param {p5.Vector} [target] vector to receive the result. * @return {p5.Vector} slerped vector between v1 and v2 */ static slerp(v1, v2, amt, target) { if (!target) { target = v1.copy(); if (arguments.length === 4) { p5._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.slerp' ); } } else { target.set(v1); } target.slerp(v2, amt); return target; } /** * Calculates the magnitude (length) of the vector and returns the result as * a float (this is simply the equation `sqrt(x*x + y*y + z*z)`.) */ /** * @static * @param {p5.Vector} vecT The vector to return the magnitude of * @return {Number} The magnitude of vecT */ static mag(vecT) { return vecT.mag(); } /** * Calculates the squared magnitude of the vector and returns the result * as a float (this is simply the equation (x\*x + y\*y + z\*z).) * Faster if the real length is not required in the * case of comparing vectors, etc. */ /** * @static * @param {p5.Vector} vecT the vector to return the squared magnitude of * @return {Number} the squared magnitude of vecT */ static magSq(vecT) { return vecT.magSq(); } /** * Normalize the vector to length 1 (make it a unit vector). */ /** * @static * @param {p5.Vector} v The vector to normalize * @param {p5.Vector} [target] The vector to receive the result * @return {p5.Vector} The vector v, normalized to a length of 1 */ static normalize(v, target) { if (arguments.length < 2) { target = v.copy(); } else { if (!(target instanceof Vector)) { p5._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.normalize' ); } target.set(v); } return target.normalize(); } /** * Limit the magnitude of the vector to the value used for the max * parameter. */ /** * @static * @param {p5.Vector} v the vector to limit * @param {Number} max * @param {p5.Vector} [target] the vector to receive the result (Optional) * @return {p5.Vector} v with a magnitude limited to max */ static limit(v, max, target) { if (arguments.length < 3) { target = v.copy(); } else { if (!(target instanceof Vector)) { p5._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.limit' ); } target.set(v); } return target.limit(max); } /** * Set the magnitude of the vector to the value used for the len * parameter. */ /** * @static * @param {p5.Vector} v the vector to set the magnitude of * @param {Number} len * @param {p5.Vector} [target] the vector to receive the result (Optional) * @return {p5.Vector} v with a magnitude set to len */ static setMag(v, len, target) { if (arguments.length < 3) { target = v.copy(); } else { if (!(target instanceof Vector)) { p5._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.setMag' ); } target.set(v); } return target.setMag(len); } /** * Calculate the angle of rotation for this vector (only 2D vectors). * p5.Vectors created using createVector() * will take the current angleMode into * consideration, and give the angle in radians or degrees accordingly. */ /** * @static * @param {p5.Vector} v the vector to find the angle of * @return {Number} the angle of rotation */ static heading(v) { return v.heading(); } /** * Calculates and returns the angle between two vectors. This function will take * the angleMode on v1 into consideration, and * give the angle in radians or degrees accordingly. */ /** * @static * @param {p5.Vector} v1 the first vector. * @param {p5.Vector} v2 the second vector. * @return {Number} angle between the two vectors. */ static angleBetween(v1, v2) { return v1.angleBetween(v2); } /** * Reflect a vector about a normal to a line in 2D, or about a normal to a * plane in 3D. */ /** * @static * @param {p5.Vector} incidentVector vector to be reflected. * @param {p5.Vector} surfaceNormal * @param {p5.Vector} [target] vector to receive the result. * @return {p5.Vector} the reflected vector */ static reflect(incidentVector, surfaceNormal, target) { if (arguments.length < 3) { target = incidentVector.copy(); } else { if (!(target instanceof Vector)) { p5._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.reflect' ); } target.set(incidentVector); } return target.reflect(surfaceNormal); } /** * Return a representation of this vector as a float array. This is only * for temporary use. If used in any other fashion, the contents should be * copied by using the p5.Vector.copy() * method to copy into your own vector. */ /** * @static * @param {p5.Vector} v the vector to convert to an array * @return {Number[]} an Array with the 3 values */ static array(v) { return v.array(); } /** * Equality check against a p5.Vector */ /** * @static * @param {p5.Vector|Array} v1 the first vector to compare * @param {p5.Vector|Array} v2 the second vector to compare * @return {Boolean} */ static equals(v1, v2) { let v; if (v1 instanceof Vector) { v = v1; } else if (v1 instanceof Array) { v = new Vector().set(v1); } else { p5._friendlyError( 'The v1 parameter should be of type Array or p5.Vector', 'p5.Vector.equals' ); } return v.equals(v2); } } function vector(p5, fn) { /** * A class to describe a two or three-dimensional vector. * * A vector can be thought of in different ways. In one view, a vector is like * an arrow pointing in space. Vectors have both magnitude (length) and * direction. * * `p5.Vector` objects are often used to program motion because they simplify * the math. For example, a moving ball has a position and a velocity. * Position describes where the ball is in space. The ball's position vector * extends from the origin to the ball's center. Velocity describes the ball's * speed and the direction it's moving. If the ball is moving straight up, its * velocity vector points straight up. Adding the ball's velocity vector to * its position vector moves it, as in `pos.add(vel)`. Vector math relies on * methods inside the `p5.Vector` class. * * Note: createVector() is the recommended way * to make an instance of this class. * * @class p5.Vector * @param {Number} [x] x component of the vector. * @param {Number} [y] y component of the vector. * @param {Number} [z] z component of the vector. * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create p5.Vector objects. * let p1 = createVector(25, 25); * let p2 = createVector(75, 75); * * // Style the points. * strokeWeight(5); * * // Draw the first point using a p5.Vector. * point(p1); * * // Draw the second point using a p5.Vector's components. * point(p2.x, p2.y); * * describe('Two black dots on a gray square, one at the top left and the other at the bottom right.'); * } * * @example * let pos; * let vel; * * function setup() { * createCanvas(100, 100); * * // Create p5.Vector objects. * pos = createVector(50, 100); * vel = createVector(0, -1); * * describe('A black dot moves from bottom to top on a gray square. The dot reappears at the bottom when it reaches the top.'); * } * * function draw() { * background(200); * * // Add velocity to position. * pos.add(vel); * * // If the dot reaches the top of the canvas, * // restart from the bottom. * if (pos.y < 0) { * pos.y = 100; * } * * // Draw the dot. * strokeWeight(5); * point(pos); * } */ p5.Vector = Vector; /** * The x component of the vector * @type {Number} * @for p5.Vector * @property x * @name x */ /** * The y component of the vector * @type {Number} * @for p5.Vector * @property y * @name y */ /** * The z component of the vector * @type {Number} * @for p5.Vector * @property z * @name z */ } if (typeof p5 !== 'undefined') { vector(p5); } /** * @module Shape * @submodule Custom Shapes * @for p5 * @requires core * @requires constants */ // ---- UTILITY FUNCTIONS ---- function polylineLength(vertices) { let length = 0; for (let i = 1; i < vertices.length; i++) { length += vertices[i-1].position.dist(vertices[i].position); } return length; } // ---- GENERAL BUILDING BLOCKS ---- class Vertex { constructor(properties) { for (const [key, value] of Object.entries(properties)) { this[key] = value; } } /* get array() { // convert to 1D array // call `toArray()` if value is an object with a toArray() method // handle primitive values separately // maybe handle object literals too, with Object.values()? // probably don’t need anything else for now? } */ // TODO: make sure name of array conversion method is // consistent with any modifications to the names of corresponding // properties of p5.Vector and p5.Color } class ShapePrimitive { vertices; _shape = null; _primitivesIndex = null; _contoursIndex = null; isClosing = false; constructor(...vertices) { if (this.constructor === ShapePrimitive) { throw new Error('ShapePrimitive is an abstract class: it cannot be instantiated.'); } if (vertices.length > 0) { this.vertices = vertices; } else { throw new Error('At least one vertex must be passed to the constructor.'); } } get vertexCount() { return this.vertices.length; } get vertexCapacity() { throw new Error('Getter vertexCapacity must be implemented.'); } get _firstInterpolatedVertex() { return this.startVertex(); } get canOverrideAnchor() { return false; } accept(visitor) { throw new Error('Method accept() must be implemented.'); } addToShape(shape) { /* TODO: Refactor? Test this method once more primitives are implemented. Test segments separately (Segment adds an extra step to this method). */ let lastContour = shape.at(-1); if (lastContour.primitives.length === 0) { lastContour.primitives.push(this); } else { // last primitive in shape let lastPrimitive = shape.at(-1, -1); let hasSameType = lastPrimitive instanceof this.constructor; let spareCapacity = lastPrimitive.vertexCapacity - lastPrimitive.vertexCount; // this primitive let pushableVertices; let remainingVertices; if (hasSameType && spareCapacity > 0) { pushableVertices = this.vertices.splice(0, spareCapacity); remainingVertices = this.vertices; lastPrimitive.vertices.push(...pushableVertices); if (remainingVertices.length > 0) { lastContour.primitives.push(this); } } else { lastContour.primitives.push(this); } } // if primitive itself was added // (i.e. its individual vertices weren't all added to an existing primitive) // give it a reference to the shape and store its location within the shape let addedToShape = this.vertices.length > 0; if (addedToShape) { let lastContour = shape.at(-1); this._primitivesIndex = lastContour.primitives.length - 1; this._contoursIndex = shape.contours.length - 1; this._shape = shape; } return shape.at(-1, -1); } get _nextPrimitive() { return this._belongsToShape ? this._shape.at(this._contoursIndex, this._primitivesIndex + 1) : null; } get _belongsToShape() { return this._shape !== null; } handlesClose() { return false; } close(vertex) { throw new Error('Unimplemented!'); } } class Contour { #kind; primitives; constructor(kind = PATH) { this.#kind = kind; this.primitives = []; } get kind() { const isEmpty = this.primitives.length === 0; const isPath = this.#kind === PATH; return isEmpty && isPath ? EMPTY_PATH : this.#kind; } accept(visitor) { for (const primitive of this.primitives) { primitive.accept(visitor); } } } // ---- PATH PRIMITIVES ---- class Anchor extends ShapePrimitive { #vertexCapacity = 1; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitAnchor(this); } getEndVertex() { return this.vertices[0]; } } // abstract class class Segment extends ShapePrimitive { constructor(...vertices) { super(...vertices); if (this.constructor === Segment) { throw new Error('Segment is an abstract class: it cannot be instantiated.'); } } // segments in a shape always have a predecessor // (either an anchor or another segment) get _previousPrimitive() { return this._belongsToShape ? this._shape.at(this._contoursIndex, this._primitivesIndex - 1) : null; } getStartVertex() { return this._previousPrimitive.getEndVertex(); } getEndVertex() { return this.vertices.at(-1); } } class LineSegment extends Segment { #vertexCapacity = 1; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitLineSegment(this); } } let BezierSegment$1 = class BezierSegment extends Segment { #order; #vertexCapacity; constructor(order, ...vertices) { super(...vertices); // Order m may sometimes be passed as an array [m], since arrays // may be used elsewhere to store order of // Bezier curves and surfaces in a common format let numericalOrder = Array.isArray(order) ? order[0] : order; this.#order = numericalOrder; this.#vertexCapacity = numericalOrder; } get order() { return this.#order; } get vertexCapacity() { return this.#vertexCapacity; } #_hullLength; hullLength() { if (this.#_hullLength === undefined) { this.#_hullLength = polylineLength([ this.getStartVertex(), ...this.vertices ]); } return this.#_hullLength; } accept(visitor) { visitor.visitBezierSegment(this); } }; /* To-do: Consider type and end modes -- see #6766 may want to use separate classes, but maybe not For now, the implementation overrides super.getEndVertex() in order to preserve current p5 endpoint behavior, but we're considering defaulting to interpolated endpoints (a breaking change) */ class SplineSegment extends Segment { #vertexCapacity = Infinity; _splineProperties = { ends: INCLUDE, tightness: 0 }; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitSplineSegment(this); } get _comesAfterSegment() { return this._previousPrimitive instanceof Segment; } get canOverrideAnchor() { return this._splineProperties.ends === EXCLUDE; } // assuming for now that the first interpolated vertex is always // the second vertex passed to splineVertex() // if this spline segment doesn't follow another segment, // the first vertex is in an anchor get _firstInterpolatedVertex() { if (this._splineProperties.ends === EXCLUDE) { return this._comesAfterSegment ? this.vertices[1] : this.vertices[0]; } else { return this.getStartVertex(); } } get _chainedToSegment() { if (this._belongsToShape && this._comesAfterSegment) { let interpolatedStartPosition = this._firstInterpolatedVertex.position; let predecessorEndPosition = this.getStartVertex().position; return predecessorEndPosition.equals(interpolatedStartPosition); } else { return false; } } // extend addToShape() with a warning in case second vertex // doesn't line up with end of last segment addToShape(shape) { const added = super.addToShape(shape); this._splineProperties.ends = shape._splineProperties.ends; this._splineProperties.tightness = shape._splineProperties.tightness; if (this._splineProperties.ends !== EXCLUDE) return added; let verticesPushed = !this._belongsToShape; let lastPrimitive = shape.at(-1, -1); let message = (array1, array2) => `Spline does not start where previous path segment ends: second spline vertex at (${array1}) expected to be at (${array2}).`; if (verticesPushed && // Only check once the first interpolated vertex has been added lastPrimitive.vertices.length === 2 && lastPrimitive._comesAfterSegment && !lastPrimitive._chainedToSegment ) { let interpolatedStart = lastPrimitive._firstInterpolatedVertex.position; let predecessorEnd = lastPrimitive.getStartVertex().position; console.warn( message(interpolatedStart.array(), predecessorEnd.array()) ); } // Note: Could add a warning in an else-if case for when this spline segment // is added directly to the shape instead of pushing its vertices to // an existing spline segment. However, if we assume addToShape() is called by // splineVertex(), it'd add a new spline segment with only one vertex in that case, // and the check wouldn't be needed yet. // TODO: Consider case where positions match but other vertex properties don't. return added; } // override method on base class getEndVertex() { if (this._splineProperties.ends === INCLUDE) { return super.getEndVertex(); } else if (this._splineProperties.ends === EXCLUDE) { return this.vertices.at(-2); } else { return this.getStartVertex(); } } getControlPoints() { let points = []; if (this._comesAfterSegment) { points.push(this.getStartVertex()); } points.push(this.getStartVertex()); for (const vertex of this.vertices) { points.push(vertex); } const prevVertex = this.getStartVertex(); if (this._splineProperties.ends === INCLUDE) { points.unshift(prevVertex); points.push(this.vertices.at(-1)); } else if (this._splineProperties.ends === JOIN) { points.unshift(this.vertices.at(-1)); points.push(prevVertex, this.vertices.at(0)); } return points; } handlesClose() { if (!this._belongsToShape) return false; // Only handle closing if the spline is the only thing in its contour after // the anchor const contour = this._shape.at(this._contoursIndex); return contour.primitives.length === 2 && this._primitivesIndex === 1; } close() { this._splineProperties.ends = JOIN; } } // ---- ISOLATED PRIMITIVES ---- class Point extends ShapePrimitive { #vertexCapacity = 1; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitPoint(this); } } class Line extends ShapePrimitive { #vertexCapacity = 2; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitLine(this); } } class Triangle extends ShapePrimitive { #vertexCapacity = 3; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitTriangle(this); } } class Quad extends ShapePrimitive { #vertexCapacity = 4; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitQuad(this); } } // ---- TESSELLATION PRIMITIVES ---- class TriangleFan extends ShapePrimitive { #vertexCapacity = Infinity; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitTriangleFan(this); } } class TriangleStrip extends ShapePrimitive { #vertexCapacity = Infinity; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitTriangleStrip(this); } } class QuadStrip extends ShapePrimitive { #vertexCapacity = Infinity; get vertexCapacity() { return this.#vertexCapacity; } accept(visitor) { visitor.visitQuadStrip(this); } } // ---- PRIMITIVE SHAPE CREATORS ---- class PrimitiveShapeCreators { // TODO: make creators private? // That'd probably be better, but for now, it may be convenient to use // native Map properties like size, e.g. for testing, and it's simpler to // not have to wrap all the properties that might be useful creators; constructor() { let creators = new Map(); /* TODO: REFACTOR BASED ON THE CODE BELOW, ONCE CONSTANTS ARE IMPLEMENTED AS SYMBOLS // Store Symbols as strings for use in Map keys const EMPTY_PATH = constants.EMPTY_PATH.description; const PATH = constants.PATH.description; //etc. creators.set(`vertex-${EMPTY_PATH}`, (...vertices) => new Anchor(...vertices)); // etc. get(vertexKind, shapeKind) { const key = `${vertexKind}-${shapeKind.description}`; return this.creators.get(key); } // etc. */ // vertex creators.set(`vertex-${EMPTY_PATH}`, (...vertices) => new Anchor(...vertices)); creators.set(`vertex-${PATH}`, (...vertices) => new LineSegment(...vertices)); creators.set(`vertex-${POINTS}`, (...vertices) => new Point(...vertices)); creators.set(`vertex-${LINES}`, (...vertices) => new Line(...vertices)); creators.set(`vertex-${TRIANGLES}`, (...vertices) => new Triangle(...vertices)); creators.set(`vertex-${QUADS}`, (...vertices) => new Quad(...vertices)); creators.set(`vertex-${TRIANGLE_FAN}`, (...vertices) => new TriangleFan(...vertices)); creators.set(`vertex-${TRIANGLE_STRIP}`, (...vertices) => new TriangleStrip(...vertices)); creators.set(`vertex-${QUAD_STRIP}`, (...vertices) => new QuadStrip(...vertices)); // bezierVertex (constructors all take order and vertices so they can be called in a uniform way) creators.set(`bezierVertex-${EMPTY_PATH}`, (order, ...vertices) => new Anchor(...vertices)); creators.set(`bezierVertex-${PATH}`, (order, ...vertices) => new BezierSegment$1(order, ...vertices)); // splineVertex creators.set(`splineVertex-${EMPTY_PATH}`, (...vertices) => new Anchor(...vertices)); creators.set(`splineVertex-${PATH}`, (...vertices) => new SplineSegment(...vertices)); this.creators = creators; } get(vertexKind, shapeKind) { const key = `${vertexKind}-${shapeKind}`; return this.creators.get(key); } set(vertexKind, shapeKind, creator) { const key = `${vertexKind}-${shapeKind}`; this.creators.set(key, creator); } clear() { this.creators.clear(); } } // ---- SHAPE ---- /* Note: It's assumed that Shape instances are always built through * their beginShape()/endShape() methods. For example, this ensures * that a segment is never the first primitive in a contour (paths * always start with an anchor), which simplifies code elsewhere. */ class Shape { #vertexProperties; #initialVertexProperties; #primitiveShapeCreators; #bezierOrder = 3; kind = null; contours = []; _splineProperties = { tightness: 0, ends: INCLUDE }; userVertexProperties = null; constructor( vertexProperties, primitiveShapeCreators = new PrimitiveShapeCreators() ) { this.#initialVertexProperties = vertexProperties; this.#vertexProperties = vertexProperties; this.#primitiveShapeCreators = primitiveShapeCreators; for (const key in this.#vertexProperties) { if (key !== 'position' && key !== 'textureCoordinates') { this[key] = function(value) { this.#vertexProperties[key] = value; }; } } } serializeToArray(val) { if (val === null || val === undefined) { return []; } if (val instanceof Number) { return [val]; } else if (val instanceof Array) { return val; } else if (val.array instanceof Function) { return val.array(); } else { throw new Error(`Can't convert ${val} to array!`); } } vertexToArray(vertex) { const array = []; for (const key in this.#vertexProperties) { if (this.userVertexProperties && key in this.userVertexProperties) continue; const val = vertex[key]; array.push(...this.serializeToArray(val)); } for (const key in this.userVertexProperties) { if (key in vertex) { array.push(...this.serializeToArray(vertex[key])); } else { array.push(...new Array(this.userVertexProperties[key]).fill(0)); } } return array; } hydrateValue(queue, original) { if (original === null) { return null; } else if (original instanceof Number) { return queue.shift(); } else if (original instanceof Array) { const array = []; for (let i = 0; i < original.length; i++) { array.push(queue.shift()); } return array; } else if (original instanceof Vector) { return new Vector(queue.shift(), queue.shift(), queue.shift()); } else if (original instanceof Color) { // NOTE: Not sure what intention here is, `Color` constructor signature // has changed so needed to be reviewed const array = [ queue.shift(), queue.shift(), queue.shift(), queue.shift() ]; return new Color(array); } } arrayToVertex(array) { const vertex = {}; const queue = [...array]; for (const key in this.#vertexProperties) { if (this.userVertexProperties && key in this.userVertexProperties) continue; const original = this.#vertexProperties[key]; vertex[key] = this.hydrateValue(queue, original); } for (const key in this.userVertexProperties) { const original = this.#vertexProperties[key]; vertex[key] = this.hydrateValue(queue, original); } return vertex; } arrayScale(array, scale) { return array.map(v => v * scale); } arraySum(first, ...rest) { return first.map((v, i) => { let result = v; for (let j = 0; j < rest.length; j++) { result += rest[j][i]; } return result; }); } arrayMinus(a, b) { return a.map((v, i) => v - b[i]); } evaluateCubicBezier([a, b, c, d], t) { return this.arraySum( this.arrayScale(a, Math.pow(1 - t, 3)), this.arrayScale(b, 3 * Math.pow(1 - t, 2) * t), this.arrayScale(c, 3 * (1 - t) * Math.pow(t, 2)), this.arrayScale(d, Math.pow(t, 3)) ); } evaluateQuadraticBezier([a, b, c], t) { return this.arraySum( this.arrayScale(a, Math.pow(1 - t, 2)), this.arrayScale(b, 2 * (1 - t) * t), this.arrayScale(c, t * t) ); } /* catmullRomToBezier(vertices, tightness) Abbreviated description: Converts a Catmull-Rom spline to a sequence of Bezier curveTo points. Parameters: vertices -> Array [v0, v1, v2, v3, ...] of at least four vertices tightness -> Number affecting shape of curve Returns: array of Bezier curveTo control points, each represented as [c1, c2, c3][] TODO: 1. It seems p5 contains code for converting from Catmull-Rom to Bezier in at least two places: catmullRomToBezier() is based on code in the legacy endShape() function: https://github.com/processing/p5.js/blob/1b66f097761d3c2057c0cec4349247d6125f93ca/src/core/p5.Renderer2D.js#L859C1-L886C1 A different conversion can be found elsewhere in p5: https://github.com/processing/p5.js/blob/17304ce9e9ef3f967bd828102a51b62a2d39d4f4/src/typography/p5.Font.js#L1179 A more careful review and comparison of both implementations would be helpful. They're different. I put catmullRomToBezier() together quickly without checking the math/algorithm, when I made the proof of concept for the refactor. 2. It may be possible to replace the code in p5.Font.js with the code here, to reduce duplication. */ catmullRomToBezier(vertices, tightness) { let s = 1 - tightness; let bezArrays = []; for (let i = 0; i + 3 < vertices.length; i++) { const [a, b, c, d] = vertices.slice(i, i + 4); const bezB = this.arraySum( b, this.arrayScale(this.arrayMinus(c, a), s / 6) ); const bezC = this.arraySum( c, this.arrayScale(this.arrayMinus(b, d), s / 6) ); const bezD = c; bezArrays.push([bezB, bezC, bezD]); } return bezArrays; } // TODO for at() method: // RENAME? // -at() indicates it works like Array.prototype.at(), e.g. with negative indices // -get() may work better if we want to add a corresponding set() method // -a set() method could maybe check for problematic usage (e.g. inserting a Triangle into a PATH) // -renaming or removing would necessitate changes at call sites (it's already in use) // REFACTOR? // TEST at(contoursIndex, primitivesIndex, verticesIndex) { let contour; let primitive; contour = this.contours.at(contoursIndex); switch(arguments.length) { case 1: return contour; case 2: return contour.primitives.at(primitivesIndex); case 3: primitive = contour.primitives.at(primitivesIndex); return primitive.vertices.at(verticesIndex); } } // maybe call this clear() for consistency with PrimitiveShapeCreators.clear()? // note: p5.Geometry has a reset() method, but also clearColors() // looks like reset() isn't in the public reference, so maybe we can switch // everything to clear()? Not sure if reset/clear is used in other classes, // but it'd be good if geometries and shapes are consistent reset() { this.#vertexProperties = { ...this.#initialVertexProperties }; this.kind = null; this.contours = []; this.userVertexProperties = null; } vertexProperty(name, data) { this.userVertexProperties = this.userVertexProperties || {}; const key = this.vertexPropertyKey(name); const dataArray = Array.isArray(data) ? data : [data]; if (!this.userVertexProperties[key]) { this.userVertexProperties[key] = dataArray.length; } this.#vertexProperties[key] = dataArray; } vertexPropertyName(key) { return key.replace(/Src$/, ''); } vertexPropertyKey(name) { return name + 'Src'; } bezierOrder(...order) { this.#bezierOrder = order; } splineProperty(key, value) { this._splineProperties[key] = value; } splineProperties(values) { if (values) { for (const key in values) { this.splineProperty(key, values[key]); } } else { return this._splineProperties; } } /* To-do: Maybe refactor #createVertex() since this has side effects that aren't advertised in the method name? */ #createVertex(position, textureCoordinates) { this.#vertexProperties.position = position; if (textureCoordinates !== undefined) { this.#vertexProperties.textureCoordinates = textureCoordinates; } return new Vertex(this.#vertexProperties); } #createPrimitiveShape(vertexKind, shapeKind, ...vertices) { let primitiveShapeCreator = this.#primitiveShapeCreators.get( vertexKind, shapeKind ); return vertexKind === 'bezierVertex' ? primitiveShapeCreator(this.#bezierOrder, ...vertices) : primitiveShapeCreator(...vertices); } /* #generalVertex() is reused by the special vertex functions, including vertex(), bezierVertex(), splineVertex(), and arcVertex(): It creates a vertex, builds a primitive including that vertex, and has the primitive add itself to the shape. */ #generalVertex(kind, position, textureCoordinates) { let vertexKind = kind; let lastContourKind = this.at(-1).kind; let vertex = this.#createVertex(position, textureCoordinates); let primitiveShape = this.#createPrimitiveShape( vertexKind, lastContourKind, vertex ); return primitiveShape.addToShape(this); } vertex(position, textureCoordinates, { isClosing = false } = {}) { const added = this.#generalVertex('vertex', position, textureCoordinates); added.isClosing = isClosing; } bezierVertex(position, textureCoordinates) { this.#generalVertex('bezierVertex', position, textureCoordinates); } splineVertex(position, textureCoordinates) { this.#generalVertex('splineVertex', position, textureCoordinates); } arcVertex(position, textureCoordinates) { this.#generalVertex('arcVertex', position, textureCoordinates); } beginContour(shapeKind = PATH) { if (this.at(-1)?.kind === EMPTY_PATH) { this.contours.pop(); } this.contours.push(new Contour(shapeKind)); } endContour(closeMode = OPEN, _index = this.contours.length - 1) { const contour = this.at(_index); if (closeMode === CLOSE) { // shape characteristics const isPath = contour.kind === PATH; // anchor characteristics const anchorVertex = this.at(_index, 0, 0); const anchorHasPosition = Object.hasOwn(anchorVertex, 'position'); const lastSegment = this.at(_index, -1); // close path if (isPath && anchorHasPosition) { if (lastSegment.handlesClose()) { lastSegment.close(anchorVertex); } else { // Temporarily remove contours after the current one so that we add to the original // contour again const rest = this.contours.splice( _index + 1, this.contours.length - _index - 1 ); const prevVertexProperties = this.#vertexProperties; this.#vertexProperties = { ...prevVertexProperties }; for (const key in anchorVertex) { if (['position', 'textureCoordinates'].includes(key)) continue; this.#vertexProperties[key] = anchorVertex[key]; } this.vertex( anchorVertex.position, anchorVertex.textureCoordinates, { isClosing: true } ); this.#vertexProperties = prevVertexProperties; this.contours.push(...rest); } } } } beginShape(shapeKind = PATH) { this.kind = shapeKind; // Implicitly start a contour this.beginContour(shapeKind); } /* TO-DO: Refactor? - Might not need anchorHasPosition. - Might combine conditions at top, and rely on shortcircuiting. Does nothing if shape is not a path or has multiple contours. Might discuss this. */ endShape(closeMode = OPEN) { if (closeMode === CLOSE) { // Close the first contour, the one implicitly used for shape data // added without an explicit contour this.endContour(closeMode, 0); } } accept(visitor) { for (const contour of this.contours) { contour.accept(visitor); } } } // ---- PRIMITIVE VISITORS ---- // abstract class class PrimitiveVisitor { constructor() { if (this.constructor === PrimitiveVisitor) { throw new Error('PrimitiveVisitor is an abstract class: it cannot be instantiated.'); } } // path primitives visitAnchor(anchor) { throw new Error('Method visitAnchor() has not been implemented.'); } visitLineSegment(lineSegment) { throw new Error('Method visitLineSegment() has not been implemented.'); } visitBezierSegment(bezierSegment) { throw new Error('Method visitBezierSegment() has not been implemented.'); } visitSplineSegment(curveSegment) { throw new Error('Method visitSplineSegment() has not been implemented.'); } visitArcSegment(arcSegment) { throw new Error('Method visitArcSegment() has not been implemented.'); } // isolated primitives visitPoint(point) { throw new Error('Method visitPoint() has not been implemented.'); } visitLine(line) { throw new Error('Method visitLine() has not been implemented.'); } visitTriangle(triangle) { throw new Error('Method visitTriangle() has not been implemented.'); } visitQuad(quad) { throw new Error('Method visitQuad() has not been implemented.'); } // tessellation primitives visitTriangleFan(triangleFan) { throw new Error('Method visitTriangleFan() has not been implemented.'); } visitTriangleStrip(triangleStrip) { throw new Error('Method visitTriangleStrip() has not been implemented.'); } visitQuadStrip(quadStrip) { throw new Error('Method visitQuadStrip() has not been implemented.'); } } // requires testing class PrimitiveToPath2DConverter extends PrimitiveVisitor { path = new Path2D(); strokeWeight; constructor({ strokeWeight }) { super(); this.strokeWeight = strokeWeight; } // path primitives visitAnchor(anchor) { let vertex = anchor.getEndVertex(); this.path.moveTo(vertex.position.x, vertex.position.y); } visitLineSegment(lineSegment) { if (lineSegment.isClosing) { // The same as lineTo, but it adds a stroke join between this // and the starting vertex rather than having two caps this.path.closePath(); } else { let vertex = lineSegment.getEndVertex(); this.path.lineTo(vertex.position.x, vertex.position.y); } } visitBezierSegment(bezierSegment) { let [v1, v2, v3] = bezierSegment.vertices; switch (bezierSegment.order) { case 2: this.path.quadraticCurveTo( v1.position.x, v1.position.y, v2.position.x, v2.position.y ); break; case 3: this.path.bezierCurveTo( v1.position.x, v1.position.y, v2.position.x, v2.position.y, v3.position.x, v3.position.y ); break; } } visitSplineSegment(splineSegment) { const shape = splineSegment._shape; if ( splineSegment._splineProperties.ends === EXCLUDE && !splineSegment._comesAfterSegment ) { let startVertex = splineSegment._firstInterpolatedVertex; this.path.moveTo(startVertex.position.x, startVertex.position.y); } const arrayVertices = splineSegment.getControlPoints().map( v => shape.vertexToArray(v) ); let bezierArrays = shape.catmullRomToBezier( arrayVertices, splineSegment._splineProperties.tightness ).map(arr => arr.map(vertArr => shape.arrayToVertex(vertArr))); for (const array of bezierArrays) { const points = array.flatMap(vert => [vert.position.x, vert.position.y]); this.path.bezierCurveTo(...points); } } visitPoint(point) { const { x, y } = point.vertices[0].position; this.path.moveTo(x, y); // Hack: to draw just strokes and not fills, draw a very very tiny line this.path.lineTo(x + 0.00001, y); } visitLine(line) { const { x: x0, y: y0 } = line.vertices[0].position; const { x: x1, y: y1 } = line.vertices[1].position; this.path.moveTo(x0, y0); this.path.lineTo(x1, y1); } visitTriangle(triangle) { const [v0, v1, v2] = triangle.vertices; this.path.moveTo(v0.position.x, v0.position.y); this.path.lineTo(v1.position.x, v1.position.y); this.path.lineTo(v2.position.x, v2.position.y); this.path.closePath(); } visitQuad(quad) { const [v0, v1, v2, v3] = quad.vertices; this.path.moveTo(v0.position.x, v0.position.y); this.path.lineTo(v1.position.x, v1.position.y); this.path.lineTo(v2.position.x, v2.position.y); this.path.lineTo(v3.position.x, v3.position.y); this.path.closePath(); } visitTriangleFan(triangleFan) { const [v0, ...rest] = triangleFan.vertices; for (let i = 0; i < rest.length - 1; i++) { const v1 = rest[i]; const v2 = rest[i + 1]; this.path.moveTo(v0.position.x, v0.position.y); this.path.lineTo(v1.position.x, v1.position.y); this.path.lineTo(v2.position.x, v2.position.y); this.path.closePath(); } } visitTriangleStrip(triangleStrip) { for (let i = 0; i < triangleStrip.vertices.length - 2; i++) { const v0 = triangleStrip.vertices[i]; const v1 = triangleStrip.vertices[i + 1]; const v2 = triangleStrip.vertices[i + 2]; this.path.moveTo(v0.position.x, v0.position.y); this.path.lineTo(v1.position.x, v1.position.y); this.path.lineTo(v2.position.x, v2.position.y); this.path.closePath(); } } visitQuadStrip(quadStrip) { for (let i = 0; i < quadStrip.vertices.length - 3; i += 2) { const v0 = quadStrip.vertices[i]; const v1 = quadStrip.vertices[i + 1]; const v2 = quadStrip.vertices[i + 2]; const v3 = quadStrip.vertices[i + 3]; this.path.moveTo(v0.position.x, v0.position.y); this.path.lineTo(v1.position.x, v1.position.y); // These are intentionally out of order to go around the quad this.path.lineTo(v3.position.x, v3.position.y); this.path.lineTo(v2.position.x, v2.position.y); this.path.closePath(); } } } class PrimitiveToVerticesConverter extends PrimitiveVisitor { contours = []; curveDetail; pointsToLines; constructor({ curveDetail = 1, pointsToLines = true } = {}) { super(); this.curveDetail = curveDetail; this.pointsToLines = pointsToLines; } lastContour() { return this.contours[this.contours.length - 1]; } visitAnchor(anchor) { this.contours.push([]); // Weird edge case: if the next segment is a spline, we might // need to jump to a different vertex. const next = anchor._nextPrimitive; if (next?.canOverrideAnchor) { this.lastContour().push(next._firstInterpolatedVertex); } else { this.lastContour().push(anchor.getEndVertex()); } } visitLineSegment(lineSegment) { this.lastContour().push(lineSegment.getEndVertex()); } visitBezierSegment(bezierSegment) { const contour = this.lastContour(); const numPoints = Math.max( 1, Math.ceil(bezierSegment.hullLength() * this.curveDetail) ); const vertexArrays = [ bezierSegment.getStartVertex(), ...bezierSegment.vertices ].map(v => bezierSegment._shape.vertexToArray(v)); for (let i = 0; i < numPoints; i++) { const t = (i + 1) / numPoints; contour.push( bezierSegment._shape.arrayToVertex( bezierSegment.order === 3 ? bezierSegment._shape.evaluateCubicBezier(vertexArrays, t) : bezierSegment._shape.evaluateQuadraticBezier(vertexArrays, t) ) ); } } visitSplineSegment(splineSegment) { const shape = splineSegment._shape; const contour = this.lastContour(); const arrayVertices = splineSegment.getControlPoints().map( v => shape.vertexToArray(v) ); let bezierArrays = shape.catmullRomToBezier( arrayVertices, splineSegment._splineProperties.tightness ); let startVertex = shape.vertexToArray( splineSegment._firstInterpolatedVertex ); for (const array of bezierArrays) { const bezierControls = [startVertex, ...array]; const numPoints = Math.max( 1, Math.ceil( polylineLength(bezierControls.map(v => shape.arrayToVertex(v))) * this.curveDetail ) ); for (let i = 0; i < numPoints; i++) { const t = (i + 1) / numPoints; contour.push( shape.arrayToVertex(shape.evaluateCubicBezier(bezierControls, t)) ); } startVertex = array[2]; } } visitPoint(point) { if (this.pointsToLines) { this.contours.push(...point.vertices.map(v => [v, v])); } else { this.contours.push(point.vertices.slice()); } } visitLine(line) { this.contours.push(line.vertices.slice()); } visitTriangle(triangle) { this.contours.push(triangle.vertices.slice()); } visitQuad(quad) { this.contours.push(quad.vertices.slice()); } visitTriangleFan(triangleFan) { // WebGL itself interprets the vertices as a fan, no reformatting needed this.contours.push(triangleFan.vertices.slice()); } visitTriangleStrip(triangleStrip) { // WebGL itself interprets the vertices as a strip, no reformatting needed this.contours.push(triangleStrip.vertices.slice()); } visitQuadStrip(quadStrip) { // WebGL itself interprets the vertices as a strip, no reformatting needed this.contours.push(quadStrip.vertices.slice()); } } class PointAtLengthGetter extends PrimitiveVisitor { constructor() { super(); } } function customShapes(p5, fn) { // ---- GENERAL CLASSES ---- /** * @private * A class to describe a custom shape made with `beginShape()`/`endShape()`. * * Every `Shape` has a `kind`. The kind takes any value that * can be passed to beginShape(): * * - `PATH` * - `POINTS` * - `LINES` * - `TRIANGLES` * - `QUADS` * - `TRIANGLE_FAN` * - `TRIANGLE_STRIP` * - `QUAD_STRIP` * * A `Shape` of any kind consists of `contours`, which can be thought of as * subshapes (shapes inside another shape). Each `contour` is built from * basic shapes called primitives, and each primitive consists of one or more vertices. * * For example, a square can be made from a single path contour with four line-segment * primitives. Each line segment contains a vertex that indicates its endpoint. A square * with a circular hole in it contains the circle in a separate contour. * * By default, each vertex only has a position, but a shape's vertices may have other * properties such as texture coordinates, a normal vector, a fill color, and a stroke color. * The properties every vertex should have may be customized by passing `vertexProperties` to * `createShape()`. * * Once a shape is created and given a name like `myShape`, it can be built up with * methods such as `myShape.beginShape()`, `myShape.vertex()`, and `myShape.endShape()`. * * Vertex functions such as `vertex()` or `bezierVertex()` are used to set the `position` * property of vertices, as well as the `textureCoordinates` property if applicable. Those * properties only apply to a single vertex. * * If `vertexProperties` includes other properties, they are each set by a method of the * same name. For example, if vertices in `myShape` have a `fill`, then that is set with * `myShape.fill()`. In the same way that a fill() may be applied * to one or more shapes, `myShape.fill()` may be applied to one or more vertices. * * @class p5.Shape * @param {Object} [vertexProperties={position: createVector(0, 0)}] vertex properties and their initial values. */ p5.Shape = Shape; /** * @private * A class to describe a contour made with `beginContour()`/`endContour()`. * * Contours may be thought of as shapes inside of other shapes. * For example, a contour may be used to create a hole in a shape that is created * with beginShape()/endShape(). * Multiple contours may be included inside a single shape. * * Contours can have any `kind` that a shape can have: * * - `PATH` * - `POINTS` * - `LINES` * - `TRIANGLES` * - `QUADS` * - `TRIANGLE_FAN` * - `TRIANGLE_STRIP` * - `QUAD_STRIP` * * By default, a contour has the same kind as the shape that contains it, but this * may be changed by passing a different `kind` to beginContour(). * * A `Contour` of any kind consists of `primitives`, which are the most basic * shapes that can be drawn. For example, if a contour is a hexagon, then * it's made from six line-segment primitives. * * @class p5.Contour */ p5.Contour = Contour; /** * @private * A base class to describe a shape primitive (a basic shape drawn with * `beginShape()`/`endShape()`). * * Shape primitives are the most basic shapes that can be drawn with * beginShape()/endShape(): * * - segment primitives: line segments, bezier segments, spline segments, and arc segments * - isolated primitives: points, lines, triangles, and quads * - tessellation primitives: triangle fans, triangle strips, and quad strips * * More complex shapes may be created by combining many primitives, possibly of different kinds, * into a single shape. * * In a similar way, every shape primitive is built from one or more vertices. * For example, a point consists of a single vertex, while a triangle consists of three vertices. * Each type of shape primitive has a `vertexCapacity`, which may be `Infinity` (for example, a * spline may consist of any number of vertices). A primitive's `vertexCount` is the number of * vertices it currently contains. * * Each primitive can add itself to a shape with an `addToShape()` method. * * It can also accept visitor objects with an `accept()` method. When a primitive accepts a visitor, * it gives the visitor access to its vertex data. For example, one visitor to a segment might turn * the data into 2D drawing instructions. Another might find a point at a given distance * along the segment. * * @class p5.ShapePrimitive * @abstract */ p5.ShapePrimitive = ShapePrimitive; /** * @private * A class to describe a vertex (a point on a shape), in 2D or 3D. * * Vertices are the basic building blocks of all `p5.Shape` objects, including * shapes made with vertex(), arcVertex(), * bezierVertex(), and splineVertex(). * * Like a point on an object in the real world, a vertex may have different properties. * These may include coordinate properties such as `position`, `textureCoordinates`, and `normal`, * color properties such as `fill` and `stroke`, and more. * * A vertex called `myVertex` with position coordinates `(2, 3, 5)` and a green stroke may be created * like this: * * ```js * let myVertex = new p5.Vertex({ * position: createVector(2, 3, 5), * stroke: color('green') * }); * ``` * * Any property names may be used. The `p5.Shape` class assumes that if a vertex has a * position or texture coordinates, they are stored in `position` and `textureCoordinates` * properties. * * Property values may be any * JavaScript primitive, any * object literal, * or any object with an `array` property. * * For example, if a position is stored as a `p5.Vector` object and a stroke is stored as a `p5.Color` object, * then the `array` properties of those objects will be used by the vertex's own `array` property, which provides * all the vertex data in a single array. * * @class p5.Vertex * @param {Object} [properties={position: createVector(0, 0)}] vertex properties. */ p5.Vertex = Vertex; // ---- PATH PRIMITIVES ---- /** * @private * A class responsible for... * * @class p5.Anchor * @extends p5.ShapePrimitive * @param {p5.Vertex} vertex the vertex to include in the anchor. */ p5.Anchor = Anchor; /** * @private * A class responsible for... * * Note: When a segment is added to a shape, it's attached to an anchor or another segment. * Adding it to another shape may result in unexpected behavior. * * @class p5.Segment * @extends p5.ShapePrimitive * @param {...p5.Vertex} vertices the vertices to include in the segment. */ p5.Segment = Segment; /** * @private * A class responsible for... * * @class p5.LineSegment * @param {p5.Vertex} vertex the vertex to include in the anchor. */ p5.LineSegment = LineSegment; /** * @private * A class responsible for... */ p5.BezierSegment = BezierSegment$1; /** * @private * A class responsible for... */ p5.SplineSegment = SplineSegment; // ---- ISOLATED PRIMITIVES ---- /** * @private * A class responsible for... */ p5.Point = Point; /** * @private * A class responsible for... * * @class p5.Line * @param {...p5.Vertex} vertices the vertices to include in the line. */ p5.Line = Line; /** * @private * A class responsible for... */ p5.Triangle = Triangle; /** * @private * A class responsible for... */ p5.Quad = Quad; // ---- TESSELLATION PRIMITIVES ---- /** * @private * A class responsible for... */ p5.TriangleFan = TriangleFan; /** * @private * A class responsible for... */ p5.TriangleStrip = TriangleStrip; /** * @private * A class responsible for... */ p5.QuadStrip = QuadStrip; // ---- PRIMITIVE VISITORS ---- /** * @private * A class responsible for... */ p5.PrimitiveVisitor = PrimitiveVisitor; /** * @private * A class responsible for... * * Notes: * 1. Assumes vertex positions are stored as p5.Vector instances. * 2. Currently only supports position properties of vectors. */ p5.PrimitiveToPath2DConverter = PrimitiveToPath2DConverter; /** * @private * A class responsible for... */ p5.PrimitiveToVerticesConverter = PrimitiveToVerticesConverter; /** * @private * A class responsible for... */ p5.PointAtLengthGetter = PointAtLengthGetter; // ---- FUNCTIONS ---- /** * Influences the shape of the Bézier curve segment in a custom shape. * By default, this is 3; the other possible parameter is 2. This * results in quadratic Bézier curves. * * `bezierVertex()` adds a curved segment to custom shapes. The Bézier curves * it creates are defined like those made by the * bezier() function. `bezierVertex()` must be * called between the * beginShape() and * endShape() functions. There must be at least * one call to bezierVertex(), before * a number of `bezierVertex()` calls that is a multiple of the parameter * set by bezierOrder(...) (default 3). * * Each curve of order 3 requires three calls to `bezierVertex`, so * 2 curves would need 7 calls to `bezierVertex()`: * (1 one initial anchor point, two sets of 3 curves describing the curves) * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2. * * Bézier curves can also be drawn in 3D using WebGL mode. * * Note: `bezierVertex()` won’t work when an argument is passed to * beginShape(). * * @method bezierOrder * @param {Number} order The new order to set. Can be either 2 or 3, by default 3 * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the shape. * noFill(); * * // Start drawing the shape. * beginShape(); * * // set the order to 2 for a quadratic Bézier curve * bezierOrder(2); * * // Add the first anchor point. * bezierVertex(30, 20); * * // Add the Bézier vertex. * bezierVertex(80, 20); * bezierVertex(50, 50); * * // Stop drawing the shape. * endShape(); * * describe('A black curve drawn on a gray square. The curve starts at the top-left corner and ends at the center.'); * } */ /** * @method bezierOrder * @returns {Number} The current Bézier order. */ fn.bezierOrder = function(order) { return this._renderer.bezierOrder(order); }; /** * Connects points with a smooth curve (a spline). * * `splineVertex()` adds a curved segment to custom shapes. * The curve it creates follows the same rules as the ones * made with the spline() function. * `splineVertex()` must be called between the * beginShape() and * endShape() functions. * * Spline curves can form shapes and curves that slope gently. They’re like * cables that are attached to a set of points. `splineVertex()` draws a smooth * curve through the points you give it. * beginShape() and * endShape() in order to draw a curve: * * * If you provide three points, the spline will pass through them. * It works the same way with any number of points. * * * * ```js * beginShape(); * * // Add the first point. * splineVertex(25, 80); * * // Add the second point. * splineVertex(20, 30); * * // Add the last point. * splineVertex(85, 60); * * endShape(); * ``` * * * * * Passing in `CLOSE` to `endShape()` closes the spline smoothly. * ```js * beginShape(); * * // Add the first point. * splineVertex(25, 80); * * // Add the second point. * splineVertex(20, 30); * * // Add the second point. * splineVertex(85, 60); * * endShape(CLOSE); * ``` * * * * * By default (`ends: INCLUDE`), the curve passes through * all the points you add with `splineVertex()`, similar to * the spline() function. To draw only * the middle span p1->p2 (skipping p0->p1 and p2->p3), set * `splineProperty('ends', EXCLUDE)`. You don’t need to duplicate * vertices to draw those spans. * * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of * `splineVertex()` has three arguments because each point has x-, y-, and * z-coordinates. By default, the vertex’s z-coordinate is set to 0. * * Note: `splineVertex()` won’t work when an argument is passed to * beginShape(). * * @method splineVertex * @param {Number} x x-coordinate of the vertex * @param {Number} y y-coordinate of the vertex * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(220); * noFill(); * strokeWeight(1); * * beginShape(); * splineVertex(25, 80); * splineVertex(20, 30); * splineVertex(85, 60); * endShape(); * * strokeWeight(5); * stroke(0); * * point(25, 80); * point(20, 30); * point(85, 60); * * describe( * 'On a gray background, a black spline passes through three marked points.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * background(220); * * beginShape(); * splineVertex(25, 80); * splineVertex(20, 30); * splineVertex(85, 60); * endShape(CLOSE); * * describe( * 'On a gray background, a closed black spline with a white interior forms a triangular shape with smooth corners.' * ); * } * * @example * let ringInnerRadius, ringWidth; * let radius, dRadius; * let theta, dTheta; * let time, dTime; * let vertexCount, unit, offset; * * function setup() { * createCanvas(400, 400); * * vertexCount = 15; * unit = createVector(1, 0); * dTheta = TAU / vertexCount; * dTime = 0.004; * * ringInnerRadius = 25; * ringWidth = 5 * ringInnerRadius; * * offset = width; * * describe( * 'A white blob with a black outline changes its shape over time.' * ); * } * * function draw() { * background(220); * strokeWeight(2); * translate(width / 2, height / 2); * * time = dTime * frameCount; * * beginShape(); * for (let i = 0; i < vertexCount; i++) { * unit.rotate(dTheta); * dRadius = noise(offset + unit.x, offset + unit.y, time) * ringWidth; * radius = ringInnerRadius + dRadius; * splineVertex(radius * unit.x, radius * unit.y); * } * endShape(CLOSE); * } * * @example * let vertexA; * let vertexB; * let vertexC; * let vertexD; * let vertexE; * let vertexF; * * let markerRadius; * * let vectorAB; * let vectorFE; * * let endOfTangentB; * let endOfTangentE; * * function setup() { * createCanvas(100, 100); * * // Initialize variables * // Adjusting vertices A and F affects the slopes at B and E * * vertexA = createVector(35, 85); * vertexB = createVector(25, 70); * vertexC = createVector(30, 30); * vertexD = createVector(70, 30); * vertexE = createVector(75, 70); * vertexF = createVector(65, 85); * * markerRadius = 4; * * vectorAB = p5.Vector.sub(vertexB, vertexA); * vectorFE = p5.Vector.sub(vertexE, vertexF); * * endOfTangentB = p5.Vector.add(vertexC, vectorAB); * endOfTangentE = p5.Vector.add(vertexD, vectorFE); * * splineProperty(`ends`, EXCLUDE); * * // Draw figure * * background(220); * * noFill(); * * beginShape(); * splineVertex(vertexA.x, vertexA.y); * splineVertex(vertexB.x, vertexB.y); * splineVertex(vertexC.x, vertexC.y); * splineVertex(vertexD.x, vertexD.y); * splineVertex(vertexE.x, vertexE.y); * splineVertex(vertexF.x, vertexF.y); * endShape(); * * stroke('red'); * line(vertexA.x, vertexA.y, vertexC.x, vertexC.y); * line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y); * * stroke('blue'); * line(vertexD.x, vertexD.y, vertexF.x, vertexF.y); * line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y); * * fill('white'); * stroke('black'); * circle(vertexA.x, vertexA.y, markerRadius); * circle(vertexB.x, vertexB.y, markerRadius); * circle(vertexC.x, vertexC.y, markerRadius); * circle(vertexD.x, vertexD.y, markerRadius); * circle(vertexE.x, vertexE.y, markerRadius); * circle(vertexF.x, vertexF.y, markerRadius); * * fill('black'); * noStroke(); * text('A', vertexA.x - 15, vertexA.y + 5); * text('B', vertexB.x - 15, vertexB.y + 5); * text('C', vertexC.x - 5, vertexC.y - 5); * text('D', vertexD.x - 5, vertexD.y - 5); * text('E', vertexE.x + 5, vertexE.y + 5); * text('F', vertexF.x + 5, vertexF.y + 5); * * describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.'); * } */ /** * @method splineVertex * @param {Number} x * @param {Number} y * @param {Number} [z] z-coordinate of the vertex. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A ghost shape drawn in white on a blue background. When the user drags the mouse, the scene rotates to reveal the outline of a second ghost.'); * } * * function draw() { * background('midnightblue'); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the first ghost. * noStroke(); * fill('ghostwhite'); * * beginShape(); * splineVertex(-28, 41, 0); * splineVertex(-28, 41, 0); * splineVertex(-29, -33, 0); * splineVertex(18, -31, 0); * splineVertex(34, 41, 0); * splineVertex(34, 41, 0); * endShape(); * * // Draw the second ghost. * noFill(); * stroke('ghostwhite'); * * beginShape(); * splineVertex(-28, 41, -20); * splineVertex(-28, 41, -20); * splineVertex(-29, -33, -20); * splineVertex(18, -31, -20); * splineVertex(34, 41, -20); * splineVertex(34, 41, -20); * endShape(); * } */ /** * @method splineVertex * @param {Number} x * @param {Number} y * @param {Number} [u=0] * @param {Number} [v=0] */ /** * @method splineVertex * @param {Number} x * @param {Number} y * @param {Number} z * @param {Number} [u=0] * @param {Number} [v=0] */ fn.splineVertex = function(...args) { let x = 0, y = 0, z = 0, u = 0, v = 0; if (args.length === 2) { [x, y] = args; } else if (args.length === 4) { [x, y, u, v] = args; } else if (args.length === 3) { [x, y, z] = args; } else if (args.length === 5) { [x, y, z, u, v] = args; } this._renderer.splineVertex(x, y, z, u, v); }; /** * Gets or sets a given spline property. * * Use `splineProperty()` to adjust the behavior of splines * created with `splineVertex()` or `spline()`. You can control * two key aspects of a spline: its end behavior (`ends`) and * its curvature (`tightness`). * * By default, the ends property is set to `INCLUDE`, which means * the spline passes through every point, including the endpoints. * You can also set it to `EXCLUDE` i.e. `splineProperty('ends', EXCLUDE)`, * which makes the spline pass through all points except the endpoints. * * `INCLUDE` case will have the spline passing through * all points, like this: * * ```js * splineProperty('ends', INCLUDE); // no need to set this, as it is the default * spline(25, 46, 93, 44, 93, 81, 35, 85); * * point(25, 46); * point(93, 44); * point(93, 81); * point(35, 85); * ``` * * * * * EXCLUDE case will have the spline passing through * the middle points, like this: * * * ```js * splineProperty('ends', EXCLUDE); * spline(25, 46, 93, 44, 93, 81, 35, 85); * * point(25, 46); * point(93, 44); * point(93, 81); * point(35, 85); * ``` * * * * By default, the tightness property is set to `0`, * producing a smooth curve that passes evenly through * the vertices. Negative values make the curve looser, * while positive values make it tighter. Common values * range between -1 and 1, though values outside this * range can also be used for different effects. * * For example, To set tightness, use `splineProperty('tightness', t)`, * (default: t = 0). * * Here's the example showing negetive value of tightness, * which creates a rounder bulge: * * ```js * splineProperty('tightness', -5) * stroke(0); * strokeWeight(2); * spline(25, 46, 93, 44, 93, 81, 35, 85); * ``` * * Here's the example showing positive value of tightness, * which makes the curve tighter and more angular: * * ```js * splineProperty('tightness', 5) * stroke(0); * strokeWeight(2); * spline(25, 46, 93, 44, 93, 81, 35, 85); * ``` * * * In all cases, the splines in p5.js are cardinal splines. * When tightness is 0, these splines are often known as * Catmull-Rom splines * * @method splineProperty * @param {String} property * @param value Value to set the given property to. * * @example * // Move the mouse left and right to see the curve change. * * let t; * * function setup() { * createCanvas(100, 100); * } * * function draw() { * background(240); * * t = map(mouseX, 0, width, -5, 5, true); * splineProperty('tightness', t); * * noFill(); * stroke(0); * strokeWeight(2); * * beginShape(); * splineVertex(10, 26); * splineVertex(83, 24); * * splineVertex(83, 61); * splineVertex(25, 65); * endShape(); * * push(); * strokeWeight(5); * point(10, 26); * point(83, 24); * point(83, 61); * point(25, 65); * pop(); * * fill(0); * noStroke(); * textSize(10); * text(`tightness: ${round(t, 1)}`, 15, 90); * describe('A black spline forms a sideways U shape through four points. The spline passes through the points more loosely as the mouse moves left of center (negative tightness), and more tightly as it moves right of center (positive tightness). The tightness is displayed at the bottom.'); * } * * @example * function setup() { * createCanvas(360, 140); * background(240); * noFill(); * * // Right panel: ends = INCLUDE (all spans). * push(); * translate(10, 10); * stroke(220); * rect(0, 0, 160, 120); * fill(30); * textSize(11); * text('ends: INCLUDE (all spans)', 8, 16); * noFill(); * * splineProperty('ends', INCLUDE); * stroke(0); * strokeWeight(2); * spline(25, 46, 93, 44, 93, 81, 35, 85); * * // vertices * strokeWeight(5); * stroke(0); * point(25, 46); * point(93, 44); * point(93, 81); * point(35, 85); * pop(); * * // Right panel: ends = EXCLUDE (middle only). * push(); * translate(190, 10); * stroke(220); * rect(0, 0, 160, 120); * noStroke(); * fill(30); * text('ends: EXCLUDE ', 18, 16); * noFill(); * * splineProperty('ends', EXCLUDE); * stroke(0); * strokeWeight(2); * spline(25, 46, 93, 44, 93, 81, 35, 85); * * // vertices * strokeWeight(5); * stroke(0); * point(25, 46); * point(93, 44); * point(93, 81); * point(35, 85); * pop(); * * describe('Left panel shows spline with ends INCLUDE (three spans). Right panel shows EXCLUDE (only the middle span). Four black points mark the vertices.'); * } * * @example * let vertexA; * let vertexB; * let vertexC; * let vertexD; * let vertexE; * let vertexF; * * let markerRadius; * * let vectorAB; * let vectorFE; * * let endOfTangentB; * let endOfTangentE; * * function setup() { * createCanvas(100, 100); * * // Initialize variables * // Adjusting vertices A and F affects the slopes at B and E * * vertexA = createVector(35, 85); * vertexB = createVector(25, 70); * vertexC = createVector(30, 30); * vertexD = createVector(70, 30); * vertexE = createVector(75, 70); * vertexF = createVector(65, 85); * * markerRadius = 4; * * vectorAB = p5.Vector.sub(vertexB, vertexA); * vectorFE = p5.Vector.sub(vertexE, vertexF); * * endOfTangentB = p5.Vector.add(vertexC, vectorAB); * endOfTangentE = p5.Vector.add(vertexD, vectorFE); * * splineProperty(`ends`, EXCLUDE); * * // Draw figure * * background(220); * * noFill(); * * beginShape(); * splineVertex(vertexA.x, vertexA.y); * splineVertex(vertexB.x, vertexB.y); * splineVertex(vertexC.x, vertexC.y); * splineVertex(vertexD.x, vertexD.y); * splineVertex(vertexE.x, vertexE.y); * splineVertex(vertexF.x, vertexF.y); * endShape(); * * stroke('red'); * line(vertexA.x, vertexA.y, vertexC.x, vertexC.y); * line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y); * * stroke('blue'); * line(vertexD.x, vertexD.y, vertexF.x, vertexF.y); * line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y); * * fill('white'); * stroke('black'); * circle(vertexA.x, vertexA.y, markerRadius); * circle(vertexB.x, vertexB.y, markerRadius); * circle(vertexC.x, vertexC.y, markerRadius); * circle(vertexD.x, vertexD.y, markerRadius); * circle(vertexE.x, vertexE.y, markerRadius); * circle(vertexF.x, vertexF.y, markerRadius); * * fill('black'); * noStroke(); * text('A', vertexA.x - 15, vertexA.y + 5); * text('B', vertexB.x - 15, vertexB.y + 5); * text('C', vertexC.x - 5, vertexC.y - 5); * text('D', vertexD.x - 5, vertexD.y - 5); * text('E', vertexE.x + 5, vertexE.y + 5); * text('F', vertexF.x + 5, vertexF.y + 5); * * describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.'); * } */ /** * @method splineProperty * @param {String} property * @returns The current value for the given property. */ fn.splineProperty = function(property, value) { return this._renderer.splineProperty(property, value); }; /** * Sets multiple properties for spline curves at once. * * `splineProperties()` accepts an object with key-value pairs to configure * how spline curves are drawn. This is a convenient way to set multiple * spline properties with a single function call, rather than calling * splineProperty() multiple times. * * The properties object can include: * - `tightness`: A number that controls how tightly the curve fits to the * vertex points. The default value is 0. Positive values make the curve * tighter (straighter), while negative values make it looser. Values * between -5 and 5 work best. * - `ends`: Controls whether to draw the end segments of the spline. Set to * `EXCLUDE` to skip drawing the segments between the first and second * points and between the second-to-last and last points. This is useful * when you want to use the first and last points as control points only. * * `splineProperties()` affects curves drawn with * splineVertex() within * beginShape() and * endShape(), as well as curves drawn with * spline(). The properties remain active until * changed by another call to `splineProperties()` or * splineProperty(). * * @method splineProperties * @param {Object} values an object containing spline property key-value pairs * @chainable * * @example * function setup() { * createCanvas(100, 100); * background(220); * * // Set spline tightness using splineProperties * splineProperties({ * tightness: 0.5 * }); * * // Draw a spline curve * noFill(); * stroke(0); * strokeWeight(2); * * beginShape(); * splineVertex(20, 80); * splineVertex(30, 30); * splineVertex(70, 30); * splineVertex(80, 80); * endShape(); * * // Show vertex points * fill(255, 0, 0); * noStroke(); * circle(20, 80, 6); * circle(30, 30, 6); * circle(70, 30, 6); * circle(80, 80, 6); * * describe('A smooth curved line with tightness 0.5 connecting four red points.'); * } * * @example * function setup() { * createCanvas(100, 100); * background(220); * * // Exclude end segments - first and last points become control points * splineProperties({ * tightness: 0, * ends: EXCLUDE * }); * * // Draw curve only between middle points * noFill(); * stroke(0); * strokeWeight(2); * * beginShape(); * splineVertex(10, 50); // Control point (affects curve but not drawn to) * splineVertex(30, 20); // Start of visible curve * splineVertex(70, 80); // End of visible curve * splineVertex(90, 50); // Control point (affects curve but not drawn to) * endShape(); * * // Show all points * fill(200, 0, 0); * noStroke(); * circle(10, 50, 6); // Control point * circle(90, 50, 6); // Control point * * fill(0, 0, 255); * circle(30, 20, 6); // Visible curve point * circle(70, 80, 6); // Visible curve point * * describe('A curved line between two blue points, with red control points at the ends.'); * } * * @method splineProperties * @return {Object} */ fn.splineProperties = function(values) { return this._renderer.splineProperties(values); }; /** * Adds a vertex to a custom shape. * * `vertex()` sets the coordinates of vertices drawn between the * beginShape() and * endShape() functions. * * The first two parameters, `x` and `y`, set the x- and y-coordinates of the * vertex. * * The third parameter, `z`, is optional. It sets the z-coordinate of the * vertex in WebGL mode. By default, `z` is 0. * * The fourth and fifth parameters, `u` and `v`, are also optional. They set * the u- and v-coordinates for the vertex’s texture when used with * endShape(). By default, `u` and `v` are both 0. * * @method vertex * @param {Number} x x-coordinate of the vertex. * @param {Number} y y-coordinate of the vertex. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the shape. * strokeWeight(3); * * // Start drawing the shape. * // Only draw the vertices. * beginShape(POINTS); * * // Add the vertices. * vertex(30, 20); * vertex(85, 20); * vertex(85, 75); * vertex(30, 75); * * // Stop drawing the shape. * endShape(); * * describe('Four black dots that form a square are drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * beginShape(); * * // Add vertices. * vertex(30, 20); * vertex(85, 20); * vertex(85, 75); * vertex(30, 75); * * // Stop drawing the shape. * endShape(CLOSE); * * describe('A white square on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Start drawing the shape. * beginShape(); * * // Add vertices. * vertex(-20, -30, 0); * vertex(35, -30, 0); * vertex(35, 25, 0); * vertex(-20, 25, 0); * * // Stop drawing the shape. * endShape(CLOSE); * * describe('A white square on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white square spins around slowly on a gray background.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Start drawing the shape. * beginShape(); * * // Add vertices. * vertex(-20, -30, 0); * vertex(35, -30, 0); * vertex(35, 25, 0); * vertex(-20, 25, 0); * * // Stop drawing the shape. * endShape(CLOSE); * } * * @example * let img; * * async function setup() { * // Load an image to apply as a texture. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100, WEBGL); * * describe('A photograph of a ceiling rotates slowly against a gray background.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Style the shape. * noStroke(); * * // Apply the texture. * texture(img); * textureMode(NORMAL); * * // Start drawing the shape * beginShape(); * * // Add vertices. * vertex(-20, -30, 0, 0, 0); * vertex(35, -30, 0, 1, 0); * vertex(35, 25, 0, 1, 1); * vertex(-20, 25, 0, 0, 1); * * // Stop drawing the shape. * endShape(); * } * * @example * let vid; * function setup() { * // Load a video and create a p5.MediaElement object. * vid = createVideo('/assets/fingers.mov'); * createCanvas(100, 100, WEBGL); * * // Hide the video. * vid.hide(); * * // Set the video to loop. * vid.loop(); * * describe('A rectangle with video as texture'); * } * * function draw() { * background(0); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Set the texture mode. * textureMode(NORMAL); * * // Apply the video as a texture. * texture(vid); * * // Draw a custom shape using uv coordinates. * beginShape(); * vertex(-40, -40, 0, 0); * vertex(40, -40, 1, 0); * vertex(40, 40, 1, 1); * vertex(-40, 40, 0, 1); * endShape(); * } */ /** * @method vertex * @param {Number} x * @param {Number} y * @param {Number} [u=0] u-coordinate of the vertex's texture. * @param {Number} [v=0] v-coordinate of the vertex's texture. */ /** * @method vertex * @param {Number} x * @param {Number} y * @param {Number} z * @param {Number} [u=0] u-coordinate of the vertex's texture. * @param {Number} [v=0] v-coordinate of the vertex's texture. */ fn.vertex = function(x, y) { let z, u, v; // default to (x, y) mode: all other arguments assumed to be 0. z = u = v = 0; if (arguments.length === 3) { // (x, y, z) mode: (u, v) assumed to be 0. z = arguments[2]; } else if (arguments.length === 4) { // (x, y, u, v) mode: z assumed to be 0. u = arguments[2]; v = arguments[3]; } else if (arguments.length === 5) { // (x, y, z, u, v) mode z = arguments[2]; u = arguments[3]; v = arguments[4]; } this._renderer.vertex(x, y, z, u, v); return; }; /** * Begins creating a hole within a flat shape. * * The `beginContour()` and endContour() * functions allow for creating negative space within custom shapes that are * flat. `beginContour()` begins adding vertices to a negative space and * endContour() stops adding them. * `beginContour()` and endContour() must be * called between beginShape() and * endShape(). * * Transformations such as translate(), * rotate(), and scale() * don't work between `beginContour()` and * endContour(). It's also not possible to use * other shapes, such as ellipse() or * rect(), between `beginContour()` and * endContour(). * * Note: The vertices that define a negative space must "wind" in the opposite * direction from the outer shape. First, draw vertices for the outer shape * clockwise order. Then, draw vertices for the negative space in * counter-clockwise order. * * @method beginContour * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * beginShape(); * * // Exterior vertices, clockwise winding. * vertex(10, 10); * vertex(90, 10); * vertex(90, 90); * vertex(10, 90); * * // Interior vertices, counter-clockwise winding. * beginContour(); * vertex(30, 30); * vertex(30, 70); * vertex(70, 70); * vertex(70, 30); * endContour(CLOSE); * * // Stop drawing the shape. * endShape(CLOSE); * * describe('A white square with a square hole in its center drawn on a gray background.'); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white square with a square hole in its center drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Start drawing the shape. * beginShape(); * * // Exterior vertices, clockwise winding. * vertex(-40, -40); * vertex(40, -40); * vertex(40, 40); * vertex(-40, 40); * * // Interior vertices, counter-clockwise winding. * beginContour(); * vertex(-20, -20); * vertex(-20, 20); * vertex(20, 20); * vertex(20, -20); * endContour(CLOSE); * * // Stop drawing the shape. * endShape(CLOSE); * } */ fn.beginContour = function(kind) { this._renderer.beginContour(kind); }; /** * Stops creating a hole within a flat shape. * * The beginContour() and `endContour()` * functions allow for creating negative space within custom shapes that are * flat. beginContour() begins adding vertices * to a negative space and `endContour()` stops adding them. * beginContour() and `endContour()` must be * called between beginShape() and * endShape(). * * By default, * the controur has an `OPEN` end, and to close it, * call `endContour(CLOSE)`. The CLOSE contour mode closes splines smoothly. * * Transformations such as translate(), * rotate(), and scale() * don't work between beginContour() and * `endContour()`. It's also not possible to use other shapes, such as * ellipse() or rect(), * between beginContour() and `endContour()`. * * Note: The vertices that define a negative space must "wind" in the opposite * direction from the outer shape. First, draw vertices for the outer shape * clockwise order. Then, draw vertices for the negative space in * counter-clockwise order. * * @method endContour * @param {OPEN|CLOSE} [mode=OPEN] By default, the value is OPEN * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * beginShape(); * * // Exterior vertices, clockwise winding. * vertex(10, 10); * vertex(90, 10); * vertex(90, 90); * vertex(10, 90); * * // Interior vertices, counter-clockwise winding. * beginContour(); * vertex(30, 30); * vertex(30, 70); * vertex(70, 70); * vertex(70, 30); * endContour(CLOSE); * * // Stop drawing the shape. * endShape(CLOSE); * * describe('A white square with a square hole in its center drawn on a gray background.'); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white square with a square hole in its center drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Start drawing the shape. * beginShape(); * * // Exterior vertices, clockwise winding. * vertex(-40, -40); * vertex(40, -40); * vertex(40, 40); * vertex(-40, 40); * * // Interior vertices, counter-clockwise winding. * beginContour(); * vertex(-20, -20); * vertex(-20, 20); * vertex(20, 20); * vertex(20, -20); * endContour(CLOSE); * * // Stop drawing the shape. * endShape(CLOSE); * } */ fn.endContour = function(mode = OPEN) { this._renderer.endContour(mode); }; } if (typeof p5 !== 'undefined') { customShapes(p5, p5.prototype); } class States { #modified = {}; constructor(initialState) { for (const key in initialState) { this[key] = initialState[key]; } } setValue(key, value) { if (!(key in this.#modified)) { this.#modified[key] = this[key]; } this[key] = value; } getDiff() { const diff = this.#modified; this.#modified = {}; return diff; } getModified() { return this.#modified; } applyDiff(prevModified) { for (const key in this.#modified) { this[key] = this.#modified[key]; } this.#modified = prevModified; } } /** * @module Rendering * @submodule Rendering * @for p5 */ class ClonableObject { constructor(obj = {}) { for (const key in obj) { this[key] = obj[key]; } } clone() { return new ClonableObject(this); } } class Renderer { static states = { strokeColor: null, strokeSet: false, fillColor: null, fillSet: false, tint: null, imageMode: CORNER, rectMode: CORNER, ellipseMode: CENTER, strokeWeight: 1, textFont: { family: 'sans-serif' }, textLeading: 15, leadingSet: false, textSize: 12, textAlign: LEFT, textBaseline: BASELINE, bezierOrder: 3, splineProperties: new ClonableObject({ ends: INCLUDE, tightness: 0 }), textWrap: WORD, // added v2.0 fontStyle: NORMAL, // v1: textStyle fontStretch: NORMAL, fontWeight: NORMAL, lineHeight: NORMAL, fontVariant: NORMAL, direction: 'inherit' }; constructor(pInst, w, h, isMainCanvas) { this._pInst = pInst; this._isMainCanvas = isMainCanvas; this.pixels = []; this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; this.width = w; this.height = h; this._events = {}; if (isMainCanvas) { this._isMainCanvas = true; } // Renderer state machine this.states = new States(Renderer.states); this.states.strokeColor = new Color([0, 0, 0]); this.states.fillColor = new Color([1, 1, 1]); this._pushPopStack = []; // NOTE: can use the length of the push pop stack instead this._pushPopDepth = 0; this._clipping = false; this._clipInvert = false; this._currentShape = undefined; // Lazily generate current shape } get currentShape() { if (!this._currentShape) { this._currentShape = new Shape(this.getCommonVertexProperties()); } return this._currentShape; } remove() { } pixelDensity(val){ let returnValue; if (typeof val === 'number') { if (val !== this._pixelDensity) { this._pixelDensity = val; } returnValue = this; this.resize(this.width, this.height); } else { returnValue = this._pixelDensity; } return returnValue; } // Makes a shallow copy of the current states // and push it into the push pop stack push() { this._pushPopDepth++; this._pushPopStack.push(this.states.getDiff()); } // Pop the previous states out of the push pop stack and // assign it back to the current state pop() { this._pushPopDepth--; const diff = this._pushPopStack.pop() || {}; const modified = this.states.getModified(); this.states.applyDiff(diff); this.updateShapeVertexProperties(modified); this.updateShapeProperties(modified); } bezierOrder(order) { if (order === undefined) { return this.states.bezierOrder; } else { this.states.setValue('bezierOrder', order); this.updateShapeProperties(); } } bezierVertex(x, y, z = 0, u = 0, v = 0) { const position = new Vector(x, y, z); const textureCoordinates = this.getSupportedIndividualVertexProperties() .textureCoordinates ? new Vector(u, v) : undefined; this.currentShape.bezierVertex(position, textureCoordinates); } splineProperty(key, value) { if (value === undefined) { return this.states.splineProperties[key]; } else { this.states.setValue('splineProperties', this.states.splineProperties.clone()); this.states.splineProperties[key] = value; } this.updateShapeProperties(); } splineProperties(values) { if (values) { for (const key in values) { this.splineProperty(key, values[key]); } } else { return { ...this.states.splineProperties }; } } splineVertex(x, y, z = 0, u = 0, v = 0) { const position = new Vector(x, y, z); const textureCoordinates = this.getSupportedIndividualVertexProperties() .textureCoordinates ? new Vector(u, v) : undefined; this.currentShape.splineVertex(position, textureCoordinates); } curveDetail(d) { if (d === undefined) { return this.states.curveDetail; } else { this.states.setValue('curveDetail', d); } } beginShape(...args) { this.currentShape.reset(); this.updateShapeVertexProperties(); this.currentShape.beginShape(...args); } endShape(...args) { this.currentShape.endShape(...args); this.drawShape(this.currentShape); } beginContour(shapeKind) { this.currentShape.beginContour(shapeKind); } endContour(mode) { this.currentShape.endContour(mode); } drawShape(shape, count) { throw new Error('Unimplemented'); } vertex(x, y, z = 0, u = 0, v = 0) { const position = new Vector(x, y, z); const textureCoordinates = this.getSupportedIndividualVertexProperties() .textureCoordinates ? new Vector(u, v) : undefined; this.currentShape.vertex(position, textureCoordinates); } bezier(x1, y1, x2, y2, x3, y3, x4, y4) { const oldOrder = this._pInst.bezierOrder(); this._pInst.bezierOrder(oldOrder); this._pInst.beginShape(); this._pInst.bezierVertex(x1, y1); this._pInst.bezierVertex(x2, y2); this._pInst.bezierVertex(x3, y3); this._pInst.bezierVertex(x4, y4); this._pInst.endShape(); return this; } spline(...args) { if (args.length === 2 * 4) { const [x1, y1, x2, y2, x3, y3, x4, y4] = args; this._pInst.beginShape(); this._pInst.splineVertex(x1, y1); this._pInst.splineVertex(x2, y2); this._pInst.splineVertex(x3, y3); this._pInst.splineVertex(x4, y4); this._pInst.endShape(); } else if (args.length === 3 * 4) { const [x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4] = args; this._pInst.beginShape(); this._pInst.splineVertex(x1, y1, z1); this._pInst.splineVertex(x2, y2, z2); this._pInst.splineVertex(x3, y3, z3); this._pInst.splineVertex(x4, y4, z4); this._pInst.endShape(); } return this; } beginClip(options = {}) { if (this._clipping) { throw new Error("It looks like you're trying to clip while already in the middle of clipping. Did you forget to endClip()?"); } this._clipping = true; this._clipInvert = options.invert; } endClip() { if (!this._clipping) { throw new Error("It looks like you've called endClip() without beginClip(). Did you forget to call beginClip() first?"); } this._clipping = false; } /** * Resize our canvas element. */ resize(w, h) { this.width = w; this.height = h; } get(x, y, w, h) { const pd = this._pixelDensity; const canvas = this.canvas; if (typeof x === 'undefined' && typeof y === 'undefined') { // get() x = y = 0; w = this.width; h = this.height; } else { x *= pd; y *= pd; if (typeof w === 'undefined' && typeof h === 'undefined') { // get(x,y) if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) { return [0, 0, 0, 0]; } return this._getPixel(x, y); } // get(x,y,w,h) } const region = new Image(w*pd, h*pd); region.pixelDensity(pd); region.canvas .getContext('2d') .drawImage(canvas, x, y, w * pd, h * pd, 0, 0, w*pd, h*pd); return region; } scale(x, y){ } fill(...args) { this.states.setValue('fillSet', true); this.states.setValue('fillColor', this._pInst.color(...args)); this.updateShapeVertexProperties(); } noFill() { this.states.setValue('fillColor', null); } strokeWeight(w) { if (w === undefined) { return this.states.strokeWeight; } else { this.states.setValue('strokeWeight', w); } } stroke(...args) { this.states.setValue('strokeSet', true); this.states.setValue('strokeColor', this._pInst.color(...args)); this.updateShapeVertexProperties(); } noStroke() { this.states.setValue('strokeColor', null); } getCommonVertexProperties() { return {}; } getSupportedIndividualVertexProperties() { return { textureCoordinates: false }; } updateShapeProperties(modified) { if (!modified || modified.bezierOrder || modified.splineProperties) { const shape = this.currentShape; shape.bezierOrder(this.states.bezierOrder); shape.splineProperty('ends', this.states.splineProperties.ends); shape.splineProperty('tightness', this.states.splineProperties.tightness); } } updateShapeVertexProperties(modified) { const props = this.getCommonVertexProperties(); if (!modified || Object.keys(modified).some(k => k in props)) { const shape = this.currentShape; for (const key in props) { shape[key](props[key]); } } } _applyDefaults() { return this; } finishDraw() { // Default no-op implementation // Override in specific renderers as needed } /////////////////////////////// //// TEXT SUPPORT METHODS ////////////////////////////// _middleAlignOffset = function() { const { textFont, textSize } = this.states; const font = textFont?.font; const ctx = this.textDrawingContext(); const metrics = ctx.measureText('X'); let sCapHeight = (font?.data || {})['OS/2']?.sCapHeight; if (sCapHeight) { const unitsPerEm = font.data.head.unitsPerEm; sCapHeight *= textSize / unitsPerEm; } else { sCapHeight = metrics.fontBoundingBoxAscent; } return metrics.alphabeticBaseline + sCapHeight / 2; }; } function renderer(p5, fn){ /** * Main graphics and rendering context, as well as the base API * implementation for p5.js "core". To be used as the superclass for * Renderer2D and Renderer3D classes, respectively. * * @class p5.Renderer * @param {HTMLElement} elt DOM node that is wrapped * @param {p5} [pInst] pointer to p5 instance * @param {Boolean} [isMainCanvas] whether we're using it as main canvas * @private */ p5.Renderer = Renderer; } /** * @module DOM * @submodule DOM */ /** * @typedef {'video'} VIDEO * @property {VIDEO} VIDEO * @final */ const VIDEO = 'video'; /** * @typedef {'audio'} AUDIO * @property {AUDIO} AUDIO * @final */ const AUDIO = 'audio'; class Cue { constructor(callback, time, id, val) { this.callback = callback; this.time = time; this.id = id; this.val = val; } } class MediaElement extends Element { constructor(elt, pInst) { super(elt, pInst); const self = this; this.elt.crossOrigin = 'anonymous'; this._prevTime = 0; this._cueIDCounter = 0; this._cues = []; this.pixels = []; this._pixelsState = this; this._pixelDensity = 1; this._modified = false; // Media has an internal canvas that is used when drawing it to the main // canvas. It will need to be updated each frame as the video itself plays. // We don't want to update it every time we draw, however, in case the user // has used load/updatePixels. To handle this, we record the frame drawn to // the internal canvas so we only update it if the frame has changed. this._frameOnCanvas = -1; Object.defineProperty(self, 'src', { get() { const firstChildSrc = self.elt.children[0].src; const srcVal = self.elt.src === window.location.href ? '' : self.elt.src; const ret = firstChildSrc === window.location.href ? srcVal : firstChildSrc; return ret; }, set(newValue) { for (let i = 0; i < self.elt.children.length; i++) { self.elt.removeChild(self.elt.children[i]); } const source = document.createElement('source'); source.src = newValue; elt.appendChild(source); self.elt.src = newValue; self._modified = true; } }); // private _onended callback, set by the method: onended(callback) self._onended = function () { }; self.elt.onended = function () { self._onended(self); }; } /** * Plays audio or video from a media element. * * @chainable * * @example * let beat; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display a message. * text('Click to play', 50, 50); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks the square.'); * } * * // Play the beat when the user presses the mouse. * function mousePressed() { * beat.play(); * } */ play() { if (this.elt.currentTime === this.elt.duration) { this.elt.currentTime = 0; } let promise; if (this.elt.readyState > 1) { promise = this.elt.play(); } else { // in Chrome, playback cannot resume after being stopped and must reload this.elt.load(); promise = this.elt.play(); } if (promise && promise.catch) { promise.catch(e => { // if it's an autoplay failure error if (e.name === 'NotAllowedError') { if (typeof IS_MINIFIED === 'undefined') { p5._friendlyAutoplayError(this.src); } else { console.error(e); } } else { // any other kind of error console.error('Media play method encountered an unexpected error', e); } }); } return this; } /** * Stops a media element and sets its current time to 0. * * Calling `media.play()` will restart playing audio/video from the beginning. * * @chainable * * @example * let beat; * let isStopped = true; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to start" written in black on a gray background. The beat starts or stops when the user presses the mouse.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isStopped === true) { * text('Click to start', 50, 50); * } else { * text('Click to stop', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isStopped === true) { * // If the beat is stopped, play it. * beat.play(); * isStopped = false; * } else { * // If the beat is playing, stop it. * beat.stop(); * isStopped = true; * } * } */ stop() { this.elt.pause(); this.elt.currentTime = 0; return this; } /** * Pauses a media element. * * Calling `media.play()` will resume playing audio/video from the moment it paused. * * @chainable * * @example * let beat; * let isPaused = true; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to play" written in black on a gray background. The beat plays or pauses when the user clicks the square.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isPaused === true) { * text('Click to play', 50, 50); * } else { * text('Click to pause', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isPaused === true) { * // If the beat is paused, * // play it. * beat.play(); * isPaused = false; * } else { * // If the beat is playing, * // pause it. * beat.pause(); * isPaused = true; * } * } */ pause() { this.elt.pause(); return this; } /** * Plays the audio/video repeatedly in a loop. * * @chainable * * @example * let beat; * let isLooping = false; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to loop" written in black on a gray background. A beat plays repeatedly in a loop when the user clicks. The beat stops when the user clicks again.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isLooping === true) { * text('Click to stop', 50, 50); * } else { * text('Click to loop', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isLooping === true) { * // If the beat is looping, stop it. * beat.stop(); * isLooping = false; * } else { * // If the beat is stopped, loop it. * beat.loop(); * isLooping = true; * } * } */ loop() { this.elt.setAttribute('loop', true); this.play(); return this; } /** * Stops the audio/video from playing in a loop. * * The media will stop when it finishes playing. * * @chainable * * @example * let beat; * let isPlaying = false; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks. The beat stops when the user clicks again.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isPlaying === true) { * text('Click to stop', 50, 50); * } else { * text('Click to play', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isPlaying === true) { * // If the beat is playing, stop it. * beat.stop(); * isPlaying = false; * } else { * // If the beat is stopped, play it. * beat.play(); * isPlaying = true; * } * } */ noLoop() { this.elt.removeAttribute('loop'); return this; } /** * Sets up logic to check that autoplay succeeded. * * @private */ _setupAutoplayFailDetection() { const timeout = setTimeout(() => { if (typeof IS_MINIFIED === 'undefined') { p5._friendlyAutoplayError(this.src); } else { console.error(e); } }, 500); this.elt.addEventListener('play', () => clearTimeout(timeout), { passive: true, once: true }); } /** * Sets the audio/video to play once it's loaded. * * The parameter, `shouldAutoplay`, is optional. Calling * `media.autoplay()` without an argument causes the media to play * automatically. If `true` is passed, as in `media.autoplay(true)`, the * media will automatically play. If `false` is passed, as in * `media.autoPlay(false)`, it won't play automatically. * * @param {Boolean} [shouldAutoplay] whether the element should autoplay. * @chainable * * @example * let video; * * function setup() { * noCanvas(); * * // Call handleVideo() once the video loads. * video = createVideo('assets/fingers.mov', handleVideo); * * describe('A video of fingers walking on a treadmill.'); * } * * // Set the video's size and play it. * function handleVideo() { * video.size(100, 100); * video.autoplay(); * } * * @example * function setup() { * noCanvas(); * * // Load a video, but don't play it automatically. * let video = createVideo('assets/fingers.mov', handleVideo); * * // Play the video when the user clicks on it. * video.mousePressed(handlePress); * * describe('An image of fingers on a treadmill. They start walking when the user double-clicks on them.'); * } * * // Set the video's size and playback mode. * function handleVideo() { * video.size(100, 100); * video.autoplay(false); * } * * // Play the video. * function handleClick() { * video.play(); * } */ autoplay(val) { const oldVal = this.elt.getAttribute('autoplay'); this.elt.setAttribute('autoplay', val); // if we turned on autoplay if (val && !oldVal) { // bind method to this scope const setupAutoplayFailDetection = () => this._setupAutoplayFailDetection(); // if media is ready to play, schedule check now if (this.elt.readyState === 4) { setupAutoplayFailDetection(); } else { // otherwise, schedule check whenever it is ready this.elt.addEventListener('canplay', setupAutoplayFailDetection, { passive: true, once: true }); } } return this; } /** * Sets the audio/video volume. * * Calling `media.volume()` without an argument returns the current volume * as a number in the range 0 (off) to 1 (maximum). * * The parameter, `val`, is optional. It's a number that sets the volume * from 0 (off) to 1 (maximum). For example, calling `media.volume(0.5)` * sets the volume to half of its maximum. * * @return {Number} current volume. * * @example * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "Volume: V" on a gray square with media controls beneath it. The number "V" oscillates between 0 and 1 as the music plays.'); * } * * function draw() { * background(200); * * // Produce a number between 0 and 1. * let n = 0.5 * sin(frameCount * 0.01) + 0.5; * * // Use n to set the volume. * dragon.volume(n); * * // Get the current volume and display it. * let v = dragon.volume(); * * // Round v to 1 decimal place for display. * v = round(v, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the volume. * text(`Volume: ${v}`, 50, 50); * } */ /** * @param {Number} val volume between 0.0 and 1.0. * @chainable */ volume(val) { if (typeof val === 'undefined') { return this.elt.volume; } else { this.elt.volume = val; } } /** * Sets the audio/video playback speed. * * The parameter, `val`, is optional. It's a number that sets the playback * speed. 1 plays the media at normal speed, 0.5 plays it at half speed, 2 * plays it at double speed, and so on. -1 plays the media at normal speed * in reverse. * * Calling `media.speed()` returns the current speed as a number. * * Note: Not all browsers support backward playback. Even if they do, * playback might not be smooth. * * @return {Number} current playback speed. * * @example * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "Speed: S" on a gray square with media controls beneath it. The number "S" oscillates between 0 and 1 as the music plays.'); * } * * function draw() { * background(200); * * // Produce a number between 0 and 2. * let n = sin(frameCount * 0.01) + 1; * * // Use n to set the playback speed. * dragon.speed(n); * * // Get the current speed and display it. * let s = dragon.speed(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the speed. * text(`Speed: ${s}`, 50, 50); * } */ /** * @param {Number} speed speed multiplier for playback. * @chainable */ speed(val) { if (typeof val === 'undefined') { return this.presetPlaybackRate || this.elt.playbackRate; } else { if (this.loadedmetadata) { this.elt.playbackRate = val; } else { this.presetPlaybackRate = val; } } } /** * Sets the media element's playback time. * * The parameter, `time`, is optional. It's a number that specifies the * time, in seconds, to jump to when playback begins. * * Calling `media.time()` without an argument returns the number of seconds * the audio/video has played. * * Note: Time resets to 0 when looping media restarts. * * @param {Number} [time] time to jump to (in seconds). * @return {Number} current time (in seconds). * * @example * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.'); * } * * function draw() { * background(200); * * // Get the current playback time. * let s = dragon.time(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the playback time. * text(`${s} seconds`, 50, 50); * } * * @example * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * // Jump to 2 seconds to start. * dragon.time(2); * * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.'); * } * * function draw() { * background(200); * * // Get the current playback time. * let s = dragon.time(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the playback time. * text(`${s} seconds`, 50, 50); * } */ time(val) { if (typeof val !== 'undefined') { this.elt.currentTime = val; } return this.elt.currentTime; } /** * Returns the audio/video's duration in seconds. * * @return {Number} duration (in seconds). * * @example * let dragon; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "S seconds left" on a gray square with media controls beneath it. The number "S" decreases as the song plays.'); * } * * function draw() { * background(200); * * // Calculate the time remaining. * let s = dragon.duration() - dragon.time(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the time remaining. * text(`${s} seconds left`, 50, 50); * } */ duration() { return this.elt.duration; } _ensureCanvas() { if (!this.canvas) { this.canvas = document.createElement('canvas'); this.drawingContext = this.canvas.getContext('2d'); this.setModified(true); } // Don't update the canvas again if we have already updated the canvas with // the current frame const needsRedraw = this._frameOnCanvas !== this._pInst.frameCount; if (this.loadedmetadata && needsRedraw) { // wait for metadata for w/h if (this.canvas.width !== this.elt.width) { this.canvas.width = this.elt.width; this.canvas.height = this.elt.height; this.width = this.canvas.width; this.height = this.canvas.height; } this.drawingContext.clearRect( 0, 0, this.canvas.width, this.canvas.height); if (this.flipped === true) { this.drawingContext.save(); this.drawingContext.scale(-1, 1); this.drawingContext.translate(-this.canvas.width, 0); } this.drawingContext.drawImage( this.elt, 0, 0, this.canvas.width, this.canvas.height ); if (this.flipped === true) { this.drawingContext.restore(); } this.setModified(true); this._frameOnCanvas = this._pInst.frameCount; } } loadPixels(...args) { this._ensureCanvas(); return p5.Renderer2D.prototype.loadPixels.apply(this, args); } updatePixels(x, y, w, h) { if (this.loadedmetadata) { // wait for metadata this._ensureCanvas(); p5.Renderer2D.prototype.updatePixels.call(this, x, y, w, h); } this.setModified(true); return this; } get(...args) { this._ensureCanvas(); return p5.Renderer2D.prototype.get.apply(this, args); } _getPixel(...args) { this.loadPixels(); return p5.Renderer2D.prototype._getPixel.apply(this, args); } set(x, y, imgOrCol) { if (this.loadedmetadata) { // wait for metadata this._ensureCanvas(); p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol); this.setModified(true); } } copy(...args) { this._ensureCanvas(); p5.prototype.copy.apply(this, args); } mask(...args) { this.loadPixels(); this.setModified(true); p5.Image.prototype.mask.apply(this, args); } /** * helper method for web GL mode to figure out if the element * has been modified and might need to be re-uploaded to texture * memory between frames. * @private * @return {boolean} a boolean indicating whether or not the * image has been updated or modified since last texture upload. */ isModified() { return this._modified; } /** * helper method for web GL mode to indicate that an element has been * changed or unchanged since last upload. gl texture upload will * set this value to false after uploading the texture; or might set * it to true if metadata has become available but there is no actual * texture data available yet.. * @param {Boolean} val sets whether or not the element has been * modified. * @private */ setModified(value) { this._modified = value; } /** * Calls a function when the audio/video reaches the end of its playback. * * The element is passed as an argument to the callback function. * * Note: The function won't be called if the media is looping. * * @param {Function} callback function to call when playback ends. * The `p5.MediaElement` is passed as * the argument. * @chainable * * @example * let beat; * let isPlaying = false; * let isDone = false; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * // Call handleEnd() when the beat finishes. * beat.onended(handleEnd); * * describe('The text "Click to play" written in black on a gray square. A beat plays when the user clicks. The text "Done!" appears when the beat finishes playing.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different messages based on playback. * if (isDone === true) { * text('Done!', 50, 50); * } else if (isPlaying === false) { * text('Click to play', 50, 50); * } else { * text('Playing...', 50, 50); * } * } * * // Play the beat when the user presses the mouse. * function mousePressed() { * if (isPlaying === false) { * isPlaying = true; * beat.play(); * } * } * * // Set isDone when playback ends. * function handleEnd() { * isDone = false; * } */ onended(callback) { this._onended = callback; return this; } /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/ _getAudioContext() { return undefined; } _getSoundOut() { return undefined; } /** * Sends the element's audio to an output. * * The parameter, `audioNode`, can be an `AudioNode` or an object from the * `p5.sound` library. * * If no element is provided, as in `myElement.connect()`, the element * connects to the main output. All connections are removed by the * `.disconnect()` method. * * Note: This method is meant to be used with the p5.sound.js addon library. * * @param {AudioNode|Object} audioNode AudioNode from the Web Audio API, * or an object from the p5.sound library */ connect(obj) { let audioContext, mainOutput; // if p5.sound exists, same audio context if (this._getAudioContext() && this._getSoundOut()) { audioContext = this._getAudioContext(); mainOutput = this._getSoundOut().input; } else { try { audioContext = obj.context; mainOutput = audioContext.destination; } catch (e) { throw 'connect() is meant to be used with Web Audio API or p5.sound.js'; } } // create a Web Audio MediaElementAudioSourceNode if none already exists if (!this.audioSourceNode) { this.audioSourceNode = audioContext.createMediaElementSource(this.elt); // connect to main output when this method is first called this.audioSourceNode.connect(mainOutput); } // connect to object if provided if (obj) { if (obj.input) { this.audioSourceNode.connect(obj.input); } else { this.audioSourceNode.connect(obj); } } else { // otherwise connect to main output of p5.sound / AudioContext this.audioSourceNode.connect(mainOutput); } } /** * Disconnect all Web Audio routing, including to the main output. * * This is useful if you want to re-route the output through audio effects, * for example. * */ disconnect() { if (this.audioSourceNode) { this.audioSourceNode.disconnect(); } else { throw 'nothing to disconnect'; } } /*** SHOW / HIDE CONTROLS ***/ /** * Show the default * HTMLMediaElement * controls. * * Note: The controls vary between web browsers. * * @example * function setup() { * createCanvas(100, 100); * * background('cornflowerblue'); * * // Style the text. * textAlign(CENTER); * textSize(50); * * // Display a dragon. * text('🐉', 50, 50); * * // Create a p5.MediaElement using createAudio(). * let dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('A dragon emoji, 🐉, drawn in the center of a blue square. A song plays in the background. Audio controls are displayed beneath the canvas.'); * } */ showControls() { // must set style for the element to show on the page this.elt.style['text-align'] = 'inherit'; this.elt.controls = true; } /** * Hide the default * HTMLMediaElement * controls. * * @example * let dragon; * let isHidden = false; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "Double-click to hide controls" written in the middle of a gray square. A song plays in the background. Audio controls are displayed beneath the canvas. The controls appear/disappear when the user double-clicks the square.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * * // Display a different message when controls are hidden or shown. * if (isHidden === true) { * text('Double-click to show controls', 10, 20, 80, 80); * } else { * text('Double-click to hide controls', 10, 20, 80, 80); * } * } * * // Show/hide controls based on a double-click. * function doubleClicked() { * if (isHidden === true) { * dragon.showControls(); * isHidden = false; * } else { * dragon.hideControls(); * isHidden = true; * } * } */ hideControls() { this.elt.controls = false; } /** * Schedules a function to call when the audio/video reaches a specific time * during its playback. * * The first parameter, `time`, is the time, in seconds, when the function * should run. This value is passed to `callback` as its first argument. * * The second parameter, `callback`, is the function to call at the specified * cue time. * * The third parameter, `value`, is optional and can be any type of value. * `value` is passed to `callback`. * * Calling `media.addCue()` returns an ID as a string. This is useful for * removing the cue later. * * @param {Number} time cue time to run the callback function. * @param {Function} callback function to call at the cue time. * @param {Object} [value] object to pass as the argument to * `callback`. * @return {Number} id ID of this cue, * useful for `media.removeCue(id)`. * * @example * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * let beat = createAudio('assets/beat.mp3'); * * // Play the beat in a loop. * beat.loop(); * * // Schedule a few events. * beat.addCue(0, changeBackground, 'red'); * beat.addCue(2, changeBackground, 'deeppink'); * beat.addCue(4, changeBackground, 'orchid'); * beat.addCue(6, changeBackground, 'lavender'); * * describe('A red square with a beat playing in the background. Its color changes every 2 seconds while the audio plays.'); * } * * // Change the background color. * function changeBackground(c) { * background(c); * } */ addCue(time, callback, val) { const id = this._cueIDCounter++; const cue = new Cue(callback, time, id, val); this._cues.push(cue); if (!this.elt.ontimeupdate) { this.elt.ontimeupdate = this._onTimeUpdate.bind(this); } return id; } /** * Removes a callback based on its ID. * * @param {Number} id ID of the cue, created by `media.addCue()`. * * @example * let lavenderID; * let isRemoved = false; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * let beat = createAudio('assets/beat.mp3'); * * // Play the beat in a loop. * beat.loop(); * * // Schedule a few events. * beat.addCue(0, changeBackground, 'red'); * beat.addCue(2, changeBackground, 'deeppink'); * beat.addCue(4, changeBackground, 'orchid'); * * // Record the ID of the "lavender" callback. * lavenderID = beat.addCue(6, changeBackground, 'lavender'); * * describe('The text "Double-click to remove lavender." written on a red square. The color changes every 2 seconds while the audio plays. The lavender option is removed when the user double-clicks the square.'); * } * * function draw() { * background(200); * * // Display different instructions based on the available callbacks. * if (isRemoved === false) { * text('Double-click to remove lavender.', 10, 10, 80, 80); * } else { * text('No more lavender.', 10, 10, 80, 80); * } * } * * // Change the background color. * function changeBackground(c) { * background(c); * } * * // Remove the lavender color-change cue when the user double-clicks. * function doubleClicked() { * if (isRemoved === false) { * beat.removeCue(lavenderID); * isRemoved = true; * } * } */ removeCue(id) { for (let i = 0; i < this._cues.length; i++) { if (this._cues[i].id === id) { console.log(id); this._cues.splice(i, 1); } } if (this._cues.length === 0) { this.elt.ontimeupdate = null; } } /** * Removes all functions scheduled with `media.addCue()`. * * @example * let isChanging = true; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * let beat = createAudio('assets/beat.mp3'); * * // Play the beat in a loop. * beat.loop(); * * // Schedule a few events. * beat.addCue(0, changeBackground, 'red'); * beat.addCue(2, changeBackground, 'deeppink'); * beat.addCue(4, changeBackground, 'orchid'); * beat.addCue(6, changeBackground, 'lavender'); * * describe('The text "Double-click to stop changing." written on a square. The color changes every 2 seconds while the audio plays. The color stops changing when the user double-clicks the square.'); * } * * function draw() { * background(200); * * // Display different instructions based on the available callbacks. * if (isChanging === true) { * text('Double-click to stop changing.', 10, 10, 80, 80); * } else { * text('No more changes.', 10, 10, 80, 80); * } * } * * // Change the background color. * function changeBackground(c) { * background(c); * } * * // Remove cued functions and stop changing colors when the user * // double-clicks. * function doubleClicked() { * if (isChanging === true) { * beat.clearCues(); * isChanging = false; * } * } */ clearCues() { this._cues = []; this.elt.ontimeupdate = null; } // private method that checks for cues to be fired if events // have been scheduled using addCue(callback, time). _onTimeUpdate() { const playbackTime = this.time(); for (let i = 0; i < this._cues.length; i++) { const callbackTime = this._cues[i].time; const val = this._cues[i].val; if (this._prevTime < callbackTime && callbackTime <= playbackTime) { // pass the scheduled callbackTime as parameter to the callback this._cues[i].callback(val); } } this._prevTime = playbackTime; } } // Cue inspired by JavaScript setTimeout, and the // Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org function media(p5, fn){ /** * Helpers for create methods. */ function addElement(elt, pInst, media) { const node = pInst._userNode ? pInst._userNode : document.body; node.appendChild(elt); const c = media ? new MediaElement(elt, pInst) : new Element(elt, pInst); pInst._elements.push(c); return c; } /* VIDEO STUFF */ // Helps perform similar tasks for media element methods. function createMedia(pInst, type, src, callback) { const elt = document.createElement(type); // Create source elements from given sources src = src || ''; if (typeof src === 'string') { src = [src]; } for (const mediaSource of src) { const sourceEl = document.createElement('source'); sourceEl.setAttribute('src', mediaSource); elt.appendChild(sourceEl); } const mediaEl = addElement(elt, pInst, true); mediaEl.loadedmetadata = false; // set width and height onload metadata elt.addEventListener('loadedmetadata', () => { mediaEl.width = elt.videoWidth; mediaEl.height = elt.videoHeight; // set elt width and height if not set if (mediaEl.elt.width === 0) mediaEl.elt.width = elt.videoWidth; if (mediaEl.elt.height === 0) mediaEl.elt.height = elt.videoHeight; if (mediaEl.presetPlaybackRate) { mediaEl.elt.playbackRate = mediaEl.presetPlaybackRate; delete mediaEl.presetPlaybackRate; } mediaEl.loadedmetadata = true; }); // If callback is provided, attach to element if (typeof callback === 'function') { const callbackHandler = () => { callback(mediaEl); elt.removeEventListener('canplaythrough', callbackHandler); }; elt.addEventListener('canplaythrough', callbackHandler); } return mediaEl; } /** * Creates a `<video>` element for simple audio/video playback. * * `createVideo()` returns a new * p5.MediaElement object. Videos are shown by * default. They can be hidden by calling `video.hide()` and drawn to the * canvas using image(). * * The first parameter, `src`, is the path the video. If a single string is * passed, as in `'assets/topsecret.mp4'`, a single video is loaded. An array * of strings can be used to load the same video in different formats. For * example, `['assets/topsecret.mp4', 'assets/topsecret.ogv', 'assets/topsecret.webm']`. * This is useful for ensuring that the video can play across different browsers with * different capabilities. See * MDN * for more information about supported formats. * * The second parameter, `callback`, is optional. It's a function to call once * the video is ready to play. * * @method createVideo * @param {String|String[]} [src] path to a video file, or an array of paths for * supporting different browsers. * @param {Function} [callback] function to call once the video is ready to play. * @return {p5.MediaElement} new p5.MediaElement object. * * @example * function setup() { * noCanvas(); * * // Load a video and add it to the page. * // Note: this may not work in some browsers. * let video = createVideo('assets/small.mp4'); * * // Show the default video controls. * video.showControls(); * * describe('A video of a toy robot with playback controls beneath it.'); * } * * @example * function setup() { * noCanvas(); * * // Load a video and add it to the page. * // Provide an array options for different file formats. * let video = createVideo( * ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm'] * ); * * // Show the default video controls. * video.showControls(); * * describe('A video of a toy robot with playback controls beneath it.'); * } * * @example * let video; * * function setup() { * noCanvas(); * * // Load a video and add it to the page. * // Provide an array options for different file formats. * // Call mute() once the video loads. * video = createVideo( * ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm'], * muteVideo * ); * * // Show the default video controls. * video.showControls(); * * describe('A video of a toy robot with playback controls beneath it.'); * } * * // Mute the video once it loads. * function muteVideo() { * video.volume(0); * } */ fn.createVideo = function (src, callback) { // p5._validateParameters('createVideo', arguments); return createMedia(this, VIDEO, src, callback); }; /* AUDIO STUFF */ /** * Creates a hidden `<audio>` element for simple audio playback. * * `createAudio()` returns a new * p5.MediaElement object. * * The first parameter, `src`, is the path the audio. If a single string is * passed, as in `'assets/audio.mp3'`, a single audio is loaded. An array * of strings can be used to load the same audio in different formats. For * example, `['assets/audio.mp3', 'assets/video.wav']`. * This is useful for ensuring that the audio can play across different * browsers with different capabilities. See * MDN * for more information about supported formats. * * The second parameter, `callback`, is optional. It's a function to call once * the audio is ready to play. * * @method createAudio * @param {String|String[]} [src] path to an audio file, or an array of paths * for supporting different browsers. * @param {Function} [callback] function to call once the audio is ready to play. * @return {p5.MediaElement} new p5.MediaElement object. * * @example * function setup() { * noCanvas(); * * // Load the audio. * let beat = createAudio('assets/beat.mp3'); * * // Show the default audio controls. * beat.showControls(); * * describe('An audio beat plays when the user double-clicks the square.'); * } */ fn.createAudio = function (src, callback) { // p5._validateParameters('createAudio', arguments); return createMedia(this, AUDIO, src, callback); }; /* CAMERA STUFF */ fn.VIDEO = VIDEO; fn.AUDIO = AUDIO; // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia // Older browsers might not implement mediaDevices at all, so we set an empty object first if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; } // Some browsers partially implement mediaDevices. We can't just assign an object // with getUserMedia as it would overwrite existing properties. // Here, we will just add the getUserMedia property if it's missing. if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia = function (constraints) { // First get ahold of the legacy getUserMedia, if present const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; // Some browsers just don't implement it - return a rejected promise with an error // to keep a consistent interface if (!getUserMedia) { return Promise.reject( new Error('getUserMedia is not implemented in this browser') ); } // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise return new Promise(function (resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); }; } /** * Creates a `<video>` element that "captures" the audio/video stream from * the webcam and microphone. * * `createCapture()` returns a new * p5.MediaElement object. Videos are shown by * default. They can be hidden by calling `capture.hide()` and drawn to the * canvas using image(). * * The first parameter, `type`, is optional. It sets the type of capture to * use. By default, `createCapture()` captures both audio and video. If `VIDEO` * is passed, as in `createCapture(VIDEO)`, only video will be captured. * If `AUDIO` is passed, as in `createCapture(AUDIO)`, only audio will be * captured. A constraints object can also be passed to customize the stream. * See the * W3C documentation for possible properties. Different browsers support different * properties. * * The 'flipped' property is an optional property which can be set to `{flipped:true}` * to mirror the video output.If it is true then it means that video will be mirrored * or flipped and if nothing is mentioned then by default it will be `false`. * * The second parameter,`callback`, is optional. It's a function to call once * the capture is ready for use. The callback function should have one * parameter, `stream`, that's a * MediaStream object. * * Note: `createCapture()` only works when running a sketch locally or using HTTPS. Learn more * here * and here. * * @method createCapture * @param {(AUDIO|VIDEO|Object)} [type] type of capture, either AUDIO or VIDEO, * or a constraints object. Both video and audio * audio streams are captured by default. * @param {Object} [flipped] flip the capturing video and mirror the output with `{flipped:true}`. By * default it is false. * @param {Function} [callback] function to call once the stream * has loaded. * @return {p5.MediaElement} new p5.MediaElement object. * * @example * function setup() { * noCanvas(); * * // Create the video capture. * createCapture(VIDEO); * * describe('A video stream from the webcam.'); * } * * @example * let capture; * * function setup() { * createCanvas(100, 100); * * // Create the video capture and hide the element. * capture = createCapture(VIDEO); * capture.hide(); * * describe('A video stream from the webcam with inverted colors.'); * } * * function draw() { * // Draw the video capture within the canvas. * image(capture, 0, 0, width, width * capture.height / capture.width); * * // Invert the colors in the stream. * filter(INVERT); * } * * @example * let capture; * * function setup() { * createCanvas(100, 100); * * // Create the video capture with mirrored output. * capture = createCapture(VIDEO,{ flipped:true }); * capture.size(100,100); * * describe('A video stream from the webcam with flipped or mirrored output.'); * } * * * @example * function setup() { * createCanvas(480, 120); * * // Create a constraints object. * let constraints = { * video: { * mandatory: { * minWidth: 1280, * minHeight: 720 * }, * optional: [{ maxFrameRate: 10 }] * }, * audio: false * }; * * // Create the video capture. * createCapture(constraints); * * describe('A video stream from the webcam.'); * } */ fn.createCapture = function (...args) { // p5._validateParameters('createCapture', args); // return if getUserMedia is not supported by the browser if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { throw new DOMException('getUserMedia not supported in this browser'); } let useVideo = true; let useAudio = true; let constraints; let callback; let flipped = false; for (const arg of args) { if (arg === fn.VIDEO) useAudio = false; else if (arg === fn.AUDIO) useVideo = false; else if (typeof arg === 'object') { if (arg.flipped !== undefined) { flipped = arg.flipped; delete arg.flipped; } constraints = Object.assign({}, constraints, arg); } else if (typeof arg === 'function') { callback = arg; } } const videoConstraints = { video: useVideo, audio: useAudio }; constraints = Object.assign({}, videoConstraints, constraints); const domElement = document.createElement(VIDEO); // required to work in iOS 11 & up: domElement.setAttribute('playsinline', ''); navigator.mediaDevices.getUserMedia(constraints).then(function (stream) { try { if ('srcObject' in domElement) { domElement.srcObject = stream; } else { domElement.src = window.URL.createObjectURL(stream); } } catch (err) { domElement.src = stream; } }).catch(e => { if (e.name === 'NotFoundError') p5._friendlyError('No webcam found on this device', 'createCapture'); if (e.name === 'NotAllowedError') p5._friendlyError('Access to the camera was denied', 'createCapture'); console.error(e); }); const videoEl = addElement(domElement, this, true); videoEl.loadedmetadata = false; // set width and height onload metadata domElement.addEventListener('loadedmetadata', function () { domElement.play(); if (domElement.width) { videoEl.width = domElement.width; videoEl.height = domElement.height; if (flipped) { videoEl.elt.style.transform = 'scaleX(-1)'; } } else { videoEl.width = videoEl.elt.width = domElement.videoWidth; videoEl.height = videoEl.elt.height = domElement.videoHeight; } videoEl.loadedmetadata = true; if (callback) callback(domElement.srcObject); }); videoEl.flipped = flipped; return videoEl; }; // ============================================================================= // p5.MediaElement additions // ============================================================================= /** * A class to handle audio and video. * * `p5.MediaElement` extends p5.Element with * methods to handle audio and video. `p5.MediaElement` objects are created by * calling createVideo, * createAudio, and * createCapture. * * @class p5.MediaElement * @param {String} elt DOM node that is wrapped * @extends p5.Element * * @example * let capture; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createCapture(). * capture = createCapture(VIDEO); * capture.hide(); * * describe('A webcam feed with inverted colors.'); * } * * function draw() { * // Display the video stream and invert the colors. * image(capture, 0, 0, width, width * capture.height / capture.width); * filter(INVERT); * } */ p5.MediaElement = MediaElement; // Patch MediaElement to give it access to fn, which p5.sound may attach things to // if present in a sketch MediaElement.prototype._getSoundOut = function() { return p5.soundOut; }; MediaElement.prototype._getAudioContext = function() { if (typeof fn.getAudioContext === 'function') { return fn.getAudioContext(); } else { return undefined; } }; /** * Path to the media element's source as a string. * * @for p5.MediaElement * @property src * @return {String} src * @example * let beat; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "https://p5js.org/reference/assets/beat.mp3" written in black on a gray background.'); * } * * function draw() { * background(200); * * textWrap(CHAR); * text(beat.src, 10, 10, 80, 80); * } */ } if(typeof p5 !== 'undefined'){ media(p5, p5.prototype); } /** * @requires constants */ /* This function normalizes the first four arguments given to rect, ellipse and arc according to the mode. It returns a 'bounding box' object containing the coordinates of the upper left corner (x, y), and width and height (w, h). The returned width and height are always positive. */ function modeAdjust(a, b, c, d, mode) { let bbox; if (mode === CORNER) { // CORNER mode already corresponds to a bounding box (top-left corner, width, height). // For negative widhts or heights, the absolute value is used. bbox = { x: a, y: b, w: Math.abs(c), h: Math.abs(d) }; } else if (mode === CORNERS) { // CORNERS mode uses two opposite corners, in any configuration. // Make sure to get the top left corner by using the minimum of the x and y coordniates. bbox = { x: Math.min(a, c), y: Math.min(b, d), w: Math.abs(c - a), h: Math.abs(d - b) }; } else if (mode === RADIUS) { // RADIUS mode uses the center point and half the width and height. // c (half width) and d (half height) could be negative, so use the absolute value // in calculating the top left corner (x, y). c = Math.abs(c); d = Math.abs(d); bbox = { x: a - c, y: b - d, w: 2 * c, h: 2 * d }; } else if (mode === CENTER) { // CENTER mode uses the center point, width and height. // c (width) and d (height) could be negative, so use the absolute value // in calculating the top-left corner (x, y). c = Math.abs(c); d = Math.abs(d); bbox = { x: a - (c * 0.5), y: b - (d * 0.5), w: c, h: d }; } return bbox; } var canvas = { modeAdjust }; /** * @module Shape * @submodule 2D Primitives * @for p5 * @requires core * @requires constants */ function primitives(p5, fn){ /** * This function does 3 things: * * 1. Bounds the desired start/stop angles for an arc (in radians) so that: * * 0 <= start < TWO_PI ; start <= stop < start + TWO_PI * * This means that the arc rendering functions don't have to be concerned * with what happens if stop is smaller than start, or if the arc 'goes * round more than once', etc.: they can just start at start and increase * until stop and the correct arc will be drawn. * * 2. Optionally adjusts the angles within each quadrant to counter the naive * scaling of the underlying ellipse up from the unit circle. Without * this, the angles become arbitrary when width != height: 45 degrees * might be drawn at 5 degrees on a 'wide' ellipse, or at 85 degrees on * a 'tall' ellipse. * * 3. Flags up when start and stop correspond to the same place on the * underlying ellipse. This is useful if you want to do something special * there (like rendering a whole ellipse instead). */ fn._normalizeArcAngles = ( start, stop, width, height, correctForScaling ) => { const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. let separation; // The order of the steps is important here: each one builds upon the // adjustments made in the steps that precede it. // Constrain both start and stop to [0,TWO_PI). start = start - TWO_PI * Math.floor(start / TWO_PI); stop = stop - TWO_PI * Math.floor(stop / TWO_PI); // Get the angular separation between the requested start and stop points. // // Technically this separation only matches what gets drawn if // correctForScaling is enabled. We could add a more complicated calculation // for when the scaling is uncorrected (in which case the drawn points could // end up pushed together or pulled apart quite dramatically relative to what // was requested), but it would make things more opaque for little practical // benefit. // // (If you do disable correctForScaling and find that correspondToSamePoint // is set too aggressively, the easiest thing to do is probably to just make // epsilon smaller...) separation = Math.min( Math.abs(start - stop), TWO_PI - Math.abs(start - stop) ); // Optionally adjust the angles to counter linear scaling. if (correctForScaling) { if (start <= HALF_PI) { start = Math.atan(width / height * Math.tan(start)); } else if (start > HALF_PI && start <= 3 * HALF_PI) { start = Math.atan(width / height * Math.tan(start)) + PI; } else { start = Math.atan(width / height * Math.tan(start)) + TWO_PI; } if (stop <= HALF_PI) { stop = Math.atan(width / height * Math.tan(stop)); } else if (stop > HALF_PI && stop <= 3 * HALF_PI) { stop = Math.atan(width / height * Math.tan(stop)) + PI; } else { stop = Math.atan(width / height * Math.tan(stop)) + TWO_PI; } } // Ensure that start <= stop < start + TWO_PI. if (start > stop) { stop += TWO_PI; } return { start, stop, correspondToSamePoint: separation < epsilon }; }; /** * Draws an arc. * * An arc is a section of an ellipse defined by the `x`, `y`, `w`, and * `h` parameters. `x` and `y` set the location of the arc's center. `w` and * `h` set the arc's width and height. See * ellipse() and * ellipseMode() for more details. * * The fifth and sixth parameters, `start` and `stop`, set the angles * between which to draw the arc. Arcs are always drawn clockwise from * `start` to `stop`. Angles are always given in radians. * * The seventh parameter, `mode`, is optional. It determines the arc's fill * style. The fill modes are a semi-circle (`OPEN`), a closed semi-circle * (`CHORD`), or a closed pie segment (`PIE`). * * The eighth parameter, `detail`, is also optional. It determines how many * vertices are used to draw the arc in WebGL mode. The default value is 25. * * @method arc * @param {Number} x x-coordinate of the arc's ellipse. * @param {Number} y y-coordinate of the arc's ellipse. * @param {Number} w width of the arc's ellipse by default. * @param {Number} h height of the arc's ellipse by default. * @param {Number} start angle to start the arc, specified in radians. * @param {Number} stop angle to stop the arc, specified in radians. * @param {(CHORD|PIE|OPEN)} [mode] optional parameter to determine the way of drawing * the arc. either CHORD, PIE, or OPEN. * @param {Integer} [detail] optional parameter for WebGL mode only. This is to * specify the number of vertices that makes up the * perimeter of the arc. Default value is 25. Won't * draw a stroke for a detail of more than 50. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * arc(50, 50, 80, 80, 0, PI + HALF_PI); * * describe('A white circle on a gray canvas. The top-right quarter of the circle is missing.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * arc(50, 50, 80, 40, 0, PI + HALF_PI); * * describe('A white ellipse on a gray canvas. The top-right quarter of the ellipse is missing.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Bottom-right. * arc(50, 55, 50, 50, 0, HALF_PI); * * noFill(); * * // Bottom-left. * arc(50, 55, 60, 60, HALF_PI, PI); * * // Top-left. * arc(50, 55, 70, 70, PI, PI + QUARTER_PI); * * // Top-right. * arc(50, 55, 80, 80, PI + QUARTER_PI, TWO_PI); * * describe( * 'A shattered outline of an circle with a quarter of a white circle at the bottom-right.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Default fill mode. * arc(50, 50, 80, 80, 0, PI + QUARTER_PI); * * describe('A white circle with the top-right third missing. The bottom is outlined in black.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // OPEN fill mode. * arc(50, 50, 80, 80, 0, PI + QUARTER_PI, OPEN); * * describe( * 'A white circle missing a section from the top-right. The bottom is outlined in black.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // CHORD fill mode. * arc(50, 50, 80, 80, 0, PI + QUARTER_PI, CHORD); * * describe('A white circle with a black outline missing a section from the top-right.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // PIE fill mode. * arc(50, 50, 80, 80, 0, PI + QUARTER_PI, PIE); * * describe('A white circle with a black outline. The top-right third is missing.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // PIE fill mode. * arc(0, 0, 80, 80, 0, PI + QUARTER_PI, PIE); * * describe('A white circle with a black outline. The top-right third is missing.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // PIE fill mode with 5 vertices. * arc(0, 0, 80, 80, 0, PI + QUARTER_PI, PIE, 5); * * describe('A white circle with a black outline. The top-right third is missing.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A yellow circle on a black background. The circle opens and closes its mouth.'); * } * * function draw() { * background(0); * * // Style the arc. * noStroke(); * fill(255, 255, 0); * * // Update start and stop angles. * let biteSize = PI / 16; * let startAngle = biteSize * sin(frameCount * 0.1) + biteSize; * let endAngle = TWO_PI - startAngle; * * // Draw the arc. * arc(50, 50, 80, 80, startAngle, endAngle, PIE); * } */ fn.arc = function(x, y, w, h, start, stop, mode, detail) { // this.validate("p5.arc", arguments); // p5._validateParameters('arc', arguments); // if the current stroke and fill settings wouldn't result in something // visible, exit immediately if ( !this._renderer.states.strokeColor && !this._renderer.states.fillColor ) { return this; } if (start === stop) { return this; } start = this._toRadians(start); stop = this._toRadians(stop); const vals = canvas.modeAdjust( x, y, w, h, this._renderer.states.ellipseMode ); const angles = this._normalizeArcAngles(start, stop, vals.w, vals.h, true); if (angles.correspondToSamePoint) { // If the arc starts and ends at (near enough) the same place, we choose to // draw an ellipse instead. This is preferable to faking an ellipse (by // making stop ever-so-slightly less than start + TWO_PI) because the ends // join up to each other rather than at a vertex at the centre (leaving // an unwanted spike in the stroke/fill). this._renderer.ellipse([vals.x, vals.y, vals.w, vals.h, detail]); } else { this._renderer.arc( vals.x, vals.y, vals.w, vals.h, angles.start, // [0, TWO_PI) angles.stop, // [start, start + TWO_PI) mode, detail ); //accessible Outputs if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._accsOutput('arc', [ vals.x, vals.y, vals.w, vals.h, angles.start, angles.stop, mode ]); } } return this; }; /** * Draws an ellipse (oval). * * An ellipse is a round shape defined by the `x`, `y`, `w`, and * `h` parameters. `x` and `y` set the location of its center. `w` and * `h` set its width and height. See * ellipseMode() for other ways to set * its position. * * If no height is set, the value of width is used for both the width and * height. If a negative height or width is specified, the absolute value is * taken. * * The fifth parameter, `detail`, is also optional. It determines how many * vertices are used to draw the ellipse in WebGL mode. The default value is * 25. * * @method ellipse * @param {Number} x x-coordinate of the center of the ellipse. * @param {Number} y y-coordinate of the center of the ellipse. * @param {Number} w width of the ellipse. * @param {Number} [h] height of the ellipse. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * ellipse(50, 50, 80, 80); * * describe('A white circle on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * ellipse(50, 50, 80); * * describe('A white circle on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * ellipse(50, 50, 80, 40); * * describe('A white ellipse on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * ellipse(0, 0, 80, 40); * * describe('A white ellipse on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Use 6 vertices. * ellipse(0, 0, 80, 40, 6); * * describe('A white hexagon on a gray canvas.'); * } */ /** * @method ellipse * @param {Number} x * @param {Number} y * @param {Number} w * @param {Number} h * @param {Integer} [detail] optional parameter for WebGL mode only. This is to * specify the number of vertices that makes up the * perimeter of the ellipse. Default value is 25. Won't * draw a stroke for a detail of more than 50. */ fn.ellipse = function(x, y, w, h, detailX) { // p5._validateParameters('ellipse', arguments); return this._renderEllipse(...arguments); }; /** * Draws a circle. * * A circle is a round shape defined by the `x`, `y`, and `d` parameters. * `x` and `y` set the location of its center. `d` sets its width and height (diameter). * Every point on the circle's edge is the same distance, `0.5 * d`, from its center. * `0.5 * d` (half the diameter) is the circle's radius. * See ellipseMode() for other ways to set its position. * * @method circle * @param {Number} x x-coordinate of the center of the circle. * @param {Number} y y-coordinate of the center of the circle. * @param {Number} d diameter of the circle. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * circle(50, 50, 25); * * describe('A white circle with black outline in the middle of a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * circle(0, 0, 25); * * describe('A white circle with black outline in the middle of a gray canvas.'); * } */ fn.circle = function(...args) { // p5._validateParameters('circle', args); const argss = args.slice( 0, 2); argss.push(args[2], args[2]); return this._renderEllipse(...argss); }; // internal method for drawing ellipses (without parameter validation) fn._renderEllipse = function(x, y, w, h, detailX) { // if the current stroke and fill settings wouldn't result in something // visible, exit immediately if ( !this._renderer.states.strokeColor && !this._renderer.states.fillColor ) { return this; } // Duplicate 3rd argument if only 3 given. if (typeof h === 'undefined') { h = w; } const vals = canvas.modeAdjust( x, y, w, h, this._renderer.states.ellipseMode ); this._renderer.ellipse([vals.x, vals.y, vals.w, vals.h, detailX]); //accessible Outputs if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._accsOutput('ellipse', [vals.x, vals.y, vals.w, vals.h]); } return this; }; /** * Draws a straight line between two points. * * A line's default width is one pixel. The version of `line()` with four * parameters draws the line in 2D. To color a line, use the * stroke() function. To change its width, use the * strokeWeight() function. A line * can't be filled, so the fill() function won't * affect the line's color. * * The version of `line()` with six parameters allows the line to be drawn in * 3D space. Doing so requires adding the `WEBGL` argument to * createCanvas(). * * @method line * @param {Number} x1 the x-coordinate of the first point. * @param {Number} y1 the y-coordinate of the first point. * @param {Number} x2 the x-coordinate of the second point. * @param {Number} y2 the y-coordinate of the second point. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * line(30, 20, 85, 75); * * describe( * 'A black line on a gray canvas running from top-center to bottom-right.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the line. * stroke('magenta'); * strokeWeight(5); * * line(30, 20, 85, 75); * * describe( * 'A thick, magenta line on a gray canvas running from top-center to bottom-right.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Top. * line(30, 20, 85, 20); * * // Right. * stroke(126); * line(85, 20, 85, 75); * * // Bottom. * stroke(255); * line(85, 75, 30, 75); * * describe( * 'Three lines drawn in grayscale on a gray canvas. They form the top, right, and bottom sides of a square.' * ); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * line(-20, -30, 35, 25); * * describe( * 'A black line on a gray canvas running from top-center to bottom-right.' * ); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A black line connecting two spheres. The scene spins slowly.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Draw a line. * line(0, 0, 0, 30, 20, -10); * * // Draw the center sphere. * sphere(10); * * // Translate to the second point. * translate(30, 20, -10); * * // Draw the bottom-right sphere. * sphere(10); * } */ /** * @method line * @param {Number} x1 * @param {Number} y1 * @param {Number} z1 the z-coordinate of the first point. * @param {Number} x2 * @param {Number} y2 * @param {Number} z2 the z-coordinate of the second point. * @chainable */ fn.line = function(...args) { // p5._validateParameters('line', args); if (this._renderer.states.strokeColor) { this._renderer.line(...args); } //accessible Outputs if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._accsOutput('line', args); } return this; }; /** * Draws a single point in space. * * A point's default width is one pixel. To color a point, use the * stroke() function. To change its width, use the * strokeWeight() function. A point * can't be filled, so the fill() function won't * affect the point's color. * * The version of `point()` with two parameters allows the point's location to * be set with its x- and y-coordinates, as in `point(10, 20)`. * * The version of `point()` with three parameters allows the point to be drawn * in 3D space with x-, y-, and z-coordinates, as in `point(10, 20, 30)`. * Doing so requires adding the `WEBGL` argument to * createCanvas(). * * The version of `point()` with one parameter allows the point's location to * be set with a p5.Vector object. * * @method point * @param {Number} x the x-coordinate. * @param {Number} y the y-coordinate. * @param {Number} [z] the z-coordinate (for WebGL mode). * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Making point to 5 pixels * strokeWeight(5); * * // Top-left. * point(30, 20); * * // Top-right. * point(85, 20); * * // Bottom-right. * point(85, 75); * * // Bottom-left. * point(30, 75); * * describe( * 'Four small, black points drawn on a gray canvas. The points form the corners of a square.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Making point to 5 pixels. * strokeWeight(5); * * // Top-left. * point(30, 20); * * // Top-right. * point(70, 20); * * // Style the next points. * stroke('purple'); * strokeWeight(10); * * // Bottom-right. * point(70, 80); * * // Bottom-left. * point(30, 80); * * describe( * 'Four points drawn on a gray canvas. Two are black and two are purple. The points form the corners of a square.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Making point to 5 pixels. * strokeWeight(5); * * // Top-left. * let a = createVector(30, 20); * point(a); * * // Top-right. * let b = createVector(70, 20); * point(b); * * // Bottom-right. * let c = createVector(70, 80); * point(c); * * // Bottom-left. * let d = createVector(30, 80); * point(d); * * describe( * 'Four small, black points drawn on a gray canvas. The points form the corners of a square.' * ); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('Two purple points drawn on a gray canvas.'); * } * * function draw() { * background(200); * * // Style the points. * stroke('purple'); * strokeWeight(10); * * // Top-left. * point(-20, -30); * * // Bottom-right. * point(20, 30); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('Two purple points drawn on a gray canvas. The scene spins slowly.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Style the points. * stroke('purple'); * strokeWeight(10); * * // Top-left. * point(-20, -30, 0); * * // Bottom-right. * point(20, 30, -50); * } */ /** * @method point * @param {p5.Vector} coordinateVector the coordinate vector. * @chainable */ fn.point = function(...args) { // p5._validateParameters('point', args); if (this._renderer.states.strokeColor) { if (args.length === 1 && args[0] instanceof p5.Vector) { this._renderer.point.call( this._renderer, args[0].x, args[0].y, args[0].z ); } else { this._renderer.point(...args); //accessible Outputs if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._accsOutput('point', args); } } } return this; }; /** * Draws a quadrilateral (four-sided shape). * * Quadrilaterals include rectangles, squares, rhombuses, and trapezoids. The * first pair of parameters `(x1, y1)` sets the quad's first point. The next * three pairs of parameters set the coordinates for its next three points * `(x2, y2)`, `(x3, y3)`, and `(x4, y4)`. Points should be added in either * clockwise or counter-clockwise order. * * The version of `quad()` with twelve parameters allows the quad to be drawn * in 3D space. Doing so requires adding the `WEBGL` argument to * createCanvas(). * * The thirteenth and fourteenth parameters are optional. In WebGL mode, they * set the number of segments used to draw the quadrilateral in the x- and * y-directions. They're both 2 by default. * * @method quad * @param {Number} x1 the x-coordinate of the first point. * @param {Number} y1 the y-coordinate of the first point. * @param {Number} x2 the x-coordinate of the second point. * @param {Number} y2 the y-coordinate of the second point. * @param {Number} x3 the x-coordinate of the third point. * @param {Number} y3 the y-coordinate of the third point. * @param {Number} x4 the x-coordinate of the fourth point. * @param {Number} y4 the y-coordinate of the fourth point. * @param {Integer} [detailX] number of segments in the x-direction. * @param {Integer} [detailY] number of segments in the y-direction. * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * quad(20, 20, 80, 20, 80, 80, 20, 80); * * describe('A white square with a black outline drawn on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * quad(20, 30, 80, 30, 80, 70, 20, 70); * * describe('A white rectangle with a black outline drawn on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * quad(50, 62, 86, 50, 50, 38, 14, 50); * * describe('A white rhombus with a black outline drawn on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * quad(20, 50, 80, 30, 80, 70, 20, 70); * * describe('A white trapezoid with a black outline drawn on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * quad(-30, -30, 30, -30, 30, 30, -30, 30); * * describe('A white square with a black outline drawn on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A wavy white surface spins around on gray canvas.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Draw the quad. * quad(-30, -30, 0, 30, -30, 0, 30, 30, 20, -30, 30, -20); * } */ /** * @method quad * @param {Number} x1 * @param {Number} y1 * @param {Number} z1 the z-coordinate of the first point. * @param {Number} x2 * @param {Number} y2 * @param {Number} z2 the z-coordinate of the second point. * @param {Number} x3 * @param {Number} y3 * @param {Number} z3 the z-coordinate of the third point. * @param {Number} x4 * @param {Number} y4 * @param {Number} z4 the z-coordinate of the fourth point. * @param {Integer} [detailX] * @param {Integer} [detailY] * @chainable */ fn.quad = function(...args) { // p5._validateParameters('quad', args); if (this._renderer.states.strokeColor || this._renderer.states.fillColor) { if (this._renderer.isP3D && args.length < 12) { // if 3D and we weren't passed 12 args, assume Z is 0 this._renderer.quad.call( this._renderer, args[0], args[1], 0, args[2], args[3], 0, args[4], args[5], 0, args[6], args[7], 0, args[8], args[9]); } else { this._renderer.quad(...args); //accessibile outputs if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._accsOutput('quadrilateral', args); } } } return this; }; /** * Draws a rectangle. * * A rectangle is a four-sided shape defined by the `x`, `y`, `w`, and `h` * parameters. `x` and `y` set the location of its top-left corner. `w` sets * its width and `h` sets its height. Every angle in the rectangle measures * 90˚. See rectMode() for other ways to define * rectangles. * * The version of `rect()` with five parameters creates a rounded rectangle. The * fifth parameter sets the radius for all four corners. * * The version of `rect()` with eight parameters also creates a rounded * rectangle. Each of the last four parameters set the radius of a corner. The * radii start with the top-left corner and move clockwise around the * rectangle. If any of these parameters are omitted, they are set to the * value of the last radius that was set. * * @method rect * @param {Number} x x-coordinate of the rectangle. * @param {Number} y y-coordinate of the rectangle. * @param {Number} w width of the rectangle. * @param {Number} [h] height of the rectangle. * @param {Number} [tl] optional radius of top-left corner. * @param {Number} [tr] optional radius of top-right corner. * @param {Number} [br] optional radius of bottom-right corner. * @param {Number} [bl] optional radius of bottom-left corner. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * rect(30, 20, 55, 55); * * describe('A white square with a black outline on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * rect(30, 20, 55, 40); * * describe('A white rectangle with a black outline on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Give all corners a radius of 20. * rect(30, 20, 55, 50, 20); * * describe('A white rectangle with a black outline and round edges on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Give each corner a unique radius. * rect(30, 20, 55, 50, 20, 15, 10, 5); * * describe('A white rectangle with a black outline and round edges of different radii.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * rect(-20, -30, 55, 55); * * describe('A white square with a black outline on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white square spins around on gray canvas.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Draw the rectangle. * rect(-20, -30, 55, 55); * } */ /** * @method rect * @param {Number} x * @param {Number} y * @param {Number} w * @param {Number} h * @param {Integer} [detailX] number of segments in the x-direction (for WebGL mode). * @param {Integer} [detailY] number of segments in the y-direction (for WebGL mode). * @chainable */ fn.rect = function(...args) { // p5._validateParameters('rect', args); return this._renderRect(...args); }; /** * Draws a square. * * A square is a four-sided shape defined by the `x`, `y`, and `s` * parameters. `x` and `y` set the location of its top-left corner. `s` sets * its width and height. Every angle in the square measures 90˚ and all its * sides are the same length. See rectMode() for * other ways to define squares. * * The version of `square()` with four parameters creates a rounded square. * The fourth parameter sets the radius for all four corners. * * The version of `square()` with seven parameters also creates a rounded * square. Each of the last four parameters set the radius of a corner. The * radii start with the top-left corner and move clockwise around the * square. If any of these parameters are omitted, they are set to the * value of the last radius that was set. * * @method square * @param {Number} x x-coordinate of the square. * @param {Number} y y-coordinate of the square. * @param {Number} s side size of the square. * @param {Number} [tl] optional radius of top-left corner. * @param {Number} [tr] optional radius of top-right corner. * @param {Number} [br] optional radius of bottom-right corner. * @param {Number} [bl] optional radius of bottom-left corner. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * square(30, 20, 55); * * describe('A white square with a black outline in on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Give all corners a radius of 20. * square(30, 20, 55, 20); * * describe( * 'A white square with a black outline and round edges on a gray canvas.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Give each corner a unique radius. * square(30, 20, 55, 20, 15, 10, 5); * * describe('A white square with a black outline and round edges of different radii.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * square(-20, -30, 55); * * describe('A white square with a black outline in on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white square spins around on gray canvas.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Draw the square. * square(-20, -30, 55); * } */ fn.square = function(x, y, s, tl, tr, br, bl) { // p5._validateParameters('square', arguments); // duplicate width for height in case of square return this._renderRect.call(this, x, y, s, s, tl, tr, br, bl); }; // internal method to have renderer draw a rectangle fn._renderRect = function() { if (this._renderer.states.strokeColor || this._renderer.states.fillColor) { // duplicate width for height in case only 3 arguments is provided if (arguments.length === 3) { arguments[3] = arguments[2]; } const vals = canvas.modeAdjust( arguments[0], arguments[1], arguments[2], arguments[3], this._renderer.states.rectMode ); // For the default rectMode (CORNER), restore a possible negative width/height // removed by modeAdjust(). This results in flipped/mirrored rendering, // which is especially noticable when using WEGBL rendering and texture(). // Note that this behavior only applies to rect(), NOT to ellipse() and arc(). if (this._renderer.states.rectMode === CORNER) { vals.w = arguments[2]; vals.h = arguments[3]; } const args = [vals.x, vals.y, vals.w, vals.h]; // append the additional arguments (either cornder radii, or // segment details) to the argument list for (let i = 4; i < arguments.length; i++) { args[i] = arguments[i]; } this._renderer.rect(args); //accessible outputs if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._accsOutput('rectangle', [vals.x, vals.y, vals.w, vals.h]); } } return this; }; /** * Draws a triangle. * * A triangle is a three-sided shape defined by three points. The * first two parameters specify the triangle's first point `(x1, y1)`. The * middle two parameters specify its second point `(x2, y2)`. And the last two * parameters specify its third point `(x3, y3)`. * * @method triangle * @param {Number} x1 x-coordinate of the first point. * @param {Number} y1 y-coordinate of the first point. * @param {Number} x2 x-coordinate of the second point. * @param {Number} y2 y-coordinate of the second point. * @param {Number} x3 x-coordinate of the third point. * @param {Number} y3 y-coordinate of the third point. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * triangle(30, 75, 58, 20, 86, 75); * * describe('A white triangle with a black outline on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * triangle(-20, 25, 8, -30, 36, 25); * * describe('A white triangle with a black outline on a gray canvas.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white triangle spins around on a gray canvas.'); * } * * function draw() { * background(200); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Draw the triangle. * triangle(-20, 25, 8, -30, 36, 25); * } */ fn.triangle = function(...args) { // p5._validateParameters('triangle', args); if (this._renderer.states.strokeColor || this._renderer.states.fillColor) { this._renderer.triangle(args); } //accessible outputs if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._accsOutput('triangle', args); } return this; }; } if(typeof p5 !== 'undefined'){ primitives(p5, p5.prototype); } /** * @module Shape * @submodule Attributes * @for p5 * @requires core * @requires constants */ function attributes(p5, fn){ /** * Changes where ellipses, circles, and arcs are drawn. * * By default, the first two parameters of * ellipse(), circle(), * and arc() * are the x- and y-coordinates of the shape's center. The next parameters set * the shape's width and height. This is the same as calling * `ellipseMode(CENTER)`. * * `ellipseMode(RADIUS)` also uses the first two parameters to set the x- and * y-coordinates of the shape's center. The next parameters are half of the * shapes's width and height. Calling `ellipse(0, 0, 10, 15)` draws a shape * with a width of 20 and height of 30. * * `ellipseMode(CORNER)` uses the first two parameters as the upper-left * corner of the shape. The next parameters are its width and height. * * `ellipseMode(CORNERS)` uses the first two parameters as the location of one * corner of the ellipse's bounding box. The next parameters are the location * of the opposite corner. * * The argument passed to `ellipseMode()` must be written in ALL CAPS because * the constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this * way. JavaScript is a case-sensitive language. * * @method ellipseMode * @param {(CENTER|RADIUS|CORNER|CORNERS)} mode either CENTER, RADIUS, CORNER, or CORNERS * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // White ellipse. * ellipseMode(RADIUS); * fill(255); * ellipse(50, 50, 30, 30); * * // Gray ellipse. * ellipseMode(CENTER); * fill(100); * ellipse(50, 50, 30, 30); * * describe('A white circle with a gray circle at its center. Both circles have black outlines.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // White ellipse. * ellipseMode(CORNER); * fill(255); * ellipse(25, 25, 50, 50); * * // Gray ellipse. * ellipseMode(CORNERS); * fill(100); * ellipse(25, 25, 50, 50); * * describe('A white circle with a gray circle at its top-left corner. Both circles have black outlines.'); * } */ fn.ellipseMode = function(m) { // p5._validateParameters('ellipseMode', arguments); if ( m === CORNER || m === CORNERS || m === RADIUS || m === CENTER ) { this._renderer.states.setValue('ellipseMode', m); } return this; }; /** * Draws certain features with jagged (aliased) edges. * * smooth() is active by default. In 2D mode, * `noSmooth()` is helpful for scaling up images without blurring. The * functions don't affect shapes or fonts. * * In WebGL mode, `noSmooth()` causes all shapes to be drawn with jagged * (aliased) edges. The functions don't affect images or fonts. * * @method noSmooth * @chainable * * @example * let heart; * * async function setup() { * // Load a pixelated heart image from an image data string. * heart = await loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII='); * createCanvas(100, 100); * * background(50); * * // Antialiased hearts. * image(heart, 10, 10); * image(heart, 20, 10, 16, 16); * image(heart, 40, 10, 32, 32); * * // Aliased hearts. * noSmooth(); * image(heart, 10, 60); * image(heart, 20, 60, 16, 16); * image(heart, 40, 60, 32, 32); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * circle(0, 0, 80); * * describe('A white circle on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * // Disable smoothing. * noSmooth(); * * background(200); * * circle(0, 0, 80); * * describe('A pixelated white circle on a gray background.'); * } */ fn.noSmooth = function() { if (!this._renderer.isP3D) { if ('imageSmoothingEnabled' in this.drawingContext) { this.drawingContext.imageSmoothingEnabled = false; } } else { this.setAttributes('antialias', false); } return this; }; /** * Changes where rectangles and squares are drawn. * * By default, the first two parameters of * rect() and square(), * are the x- and y-coordinates of the shape's upper left corner. The next parameters set * the shape's width and height. This is the same as calling * `rectMode(CORNER)`. * * `rectMode(CORNERS)` also uses the first two parameters as the location of * one of the corners. The next parameters are the location of the opposite * corner. This mode only works for rect(). * * `rectMode(CENTER)` uses the first two parameters as the x- and * y-coordinates of the shape's center. The next parameters are its width and * height. * * `rectMode(RADIUS)` also uses the first two parameters as the x- and * y-coordinates of the shape's center. The next parameters are * half of the shape's width and height. * * The argument passed to `rectMode()` must be written in ALL CAPS because the * constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this way. * JavaScript is a case-sensitive language. * * @method rectMode * @param {(CENTER|RADIUS|CORNER|CORNERS)} mode either CORNER, CORNERS, CENTER, or RADIUS * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * rectMode(CORNER); * fill(255); * rect(25, 25, 50, 50); * * rectMode(CORNERS); * fill(100); * rect(25, 25, 50, 50); * * describe('A small gray square drawn at the top-left corner of a white square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * rectMode(RADIUS); * fill(255); * rect(50, 50, 30, 30); * * rectMode(CENTER); * fill(100); * rect(50, 50, 30, 30); * * describe('A small gray square drawn at the center of a white square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * rectMode(CORNER); * fill(255); * square(25, 25, 50); * * describe('A white square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * rectMode(RADIUS); * fill(255); * square(50, 50, 30); * * rectMode(CENTER); * fill(100); * square(50, 50, 30); * * describe('A small gray square drawn at the center of a white square.'); * } */ fn.rectMode = function(m) { // p5._validateParameters('rectMode', arguments); if ( m === CORNER || m === CORNERS || m === RADIUS || m === CENTER ) { this._renderer.states.setValue('rectMode', m); } return this; // return current rectMode ? }; /** * Draws certain features with smooth (antialiased) edges. * * `smooth()` is active by default. In 2D mode, * noSmooth() is helpful for scaling up images * without blurring. The functions don't affect shapes or fonts. * * In WebGL mode, noSmooth() causes all shapes to * be drawn with jagged (aliased) edges. The functions don't affect images or * fonts. * * @method smooth * @chainable * * @example * let heart; * * async function setup() { * // Load a pixelated heart image from an image data string. * heart = await loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII='); * * createCanvas(100, 100); * * background(50); * * // Antialiased hearts. * image(heart, 10, 10); * image(heart, 20, 10, 16, 16); * image(heart, 40, 10, 32, 32); * * // Aliased hearts. * noSmooth(); * image(heart, 10, 60); * image(heart, 20, 60, 16, 16); * image(heart, 40, 60, 32, 32); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * circle(0, 0, 80); * * describe('A white circle on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * // Disable smoothing. * noSmooth(); * * background(200); * * circle(0, 0, 80); * * describe('A pixelated white circle on a gray background.'); * } */ fn.smooth = function() { if (!this._renderer.isP3D) { if ('imageSmoothingEnabled' in this.drawingContext) { this.drawingContext.imageSmoothingEnabled = true; } } else { this.setAttributes('antialias', true); } return this; }; /** * Sets the style for rendering the ends of lines. * * The caps for line endings are either rounded (`ROUND`), squared * (`SQUARE`), or extended (`PROJECT`). The default cap is `ROUND`. * * The argument passed to `strokeCap()` must be written in ALL CAPS because * the constants `ROUND`, `SQUARE`, and `PROJECT` are defined this way. * JavaScript is a case-sensitive language. * * @method strokeCap * @param {(ROUND|SQUARE|PROJECT)} cap either ROUND, SQUARE, or PROJECT * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * strokeWeight(12); * * // Top. * strokeCap(ROUND); * line(20, 30, 80, 30); * * // Middle. * strokeCap(SQUARE); * line(20, 50, 80, 50); * * // Bottom. * strokeCap(PROJECT); * line(20, 70, 80, 70); * * describe( * 'Three horizontal lines. The top line has rounded ends, the middle line has squared ends, and the bottom line has longer, squared ends.' * ); * } */ fn.strokeCap = function(cap) { // p5._validateParameters('strokeCap', arguments); if ( cap === ROUND || cap === SQUARE || cap === PROJECT ) { this._renderer.strokeCap(cap); } return this; }; /** * Sets the style of the joints that connect line segments. * * Joints are either mitered (`MITER`), beveled (`BEVEL`), or rounded * (`ROUND`). The default joint is `MITER` in 2D mode and `ROUND` in WebGL * mode. * * The argument passed to `strokeJoin()` must be written in ALL CAPS because * the constants `MITER`, `BEVEL`, and `ROUND` are defined this way. * JavaScript is a case-sensitive language. * * @method strokeJoin * @param {(MITER|BEVEL|ROUND)} join either MITER, BEVEL, or ROUND * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the line. * noFill(); * strokeWeight(10); * strokeJoin(MITER); * * // Draw the line. * beginShape(); * vertex(35, 20); * vertex(65, 50); * vertex(35, 80); * endShape(); * * describe('A right-facing arrowhead shape with a pointed tip in center of canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the line. * noFill(); * strokeWeight(10); * strokeJoin(BEVEL); * * // Draw the line. * beginShape(); * vertex(35, 20); * vertex(65, 50); * vertex(35, 80); * endShape(); * * describe('A right-facing arrowhead shape with a flat tip in center of canvas.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the line. * noFill(); * strokeWeight(10); * strokeJoin(ROUND); * * // Draw the line. * beginShape(); * vertex(35, 20); * vertex(65, 50); * vertex(35, 80); * endShape(); * * describe('A right-facing arrowhead shape with a rounded tip in center of canvas.'); * } */ fn.strokeJoin = function(join) { // p5._validateParameters('strokeJoin', arguments); if ( join === ROUND || join === BEVEL || join === MITER ) { this._renderer.strokeJoin(join); } return this; }; /** * Sets the width of the stroke used for points, lines, and the outlines of * shapes. * * Note: `strokeWeight()` is affected by transformations, especially calls to * scale(). * * @method strokeWeight * @param {Number} weight the weight of the stroke (in pixels). * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Top. * line(20, 20, 80, 20); * * // Middle. * strokeWeight(4); * line(20, 40, 80, 40); * * // Bottom. * strokeWeight(10); * line(20, 70, 80, 70); * * describe('Three horizontal black lines. The top line is thin, the middle is medium, and the bottom is thick.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Top. * line(20, 20, 80, 20); * * // Scale by a factor of 5. * scale(5); * * // Bottom. Coordinates are adjusted for scaling. * line(4, 8, 16, 8); * * describe('Two horizontal black lines. The top line is thin and the bottom is five times thicker than the top.'); * } */ fn.strokeWeight = function(w) { // p5._validateParameters('strokeWeight', arguments); this._renderer.strokeWeight(w); return this; }; } if(typeof p5 !== 'undefined'){ attributes(p5, p5.prototype); } /** * @module Shape * @submodule Curves * @for p5 * @requires core */ function curves(p5, fn){ /** * Draws a Bézier curve. * * Bézier curves can form shapes and curves that slope gently. They're defined * by two anchor points and two control points. Bézier curves provide more * control than the spline curves created with the * spline() function. * * The first two parameters, `x1` and `y1`, set the first anchor point. The * first anchor point is where the curve starts. * * The next four parameters, `x2`, `y2`, `x3`, and `y3`, set the two control * points. The control points "pull" the curve towards them. * * The seventh and eighth parameters, `x4` and `y4`, set the last anchor * point. The last anchor point is where the curve ends. * * Bézier curves can also be drawn in 3D using WebGL mode. The 3D version of * `bezier()` has twelve arguments because each point has x-, y-, * and z-coordinates. * * @method bezier * @param {Number} x1 x-coordinate of the first anchor point. * @param {Number} y1 y-coordinate of the first anchor point. * @param {Number} x2 x-coordinate of the first control point. * @param {Number} y2 y-coordinate of the first control point. * @param {Number} x3 x-coordinate of the second control point. * @param {Number} y3 y-coordinate of the second control point. * @param {Number} x4 x-coordinate of the second anchor point. * @param {Number} y4 y-coordinate of the second anchor point. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the anchor points in black. * stroke(0); * strokeWeight(5); * point(85, 20); * point(15, 80); * * // Draw the control points in red. * stroke(255, 0, 0); * point(10, 10); * point(90, 90); * * // Draw a black bezier curve. * noFill(); * stroke(0); * strokeWeight(1); * bezier(85, 20, 10, 10, 90, 90, 15, 80); * * // Draw red lines from the anchor points to the control points. * stroke(255, 0, 0); * line(85, 20, 10, 10); * line(15, 80, 90, 90); * * describe( * 'A gray square with three curves. A black s-curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.' * ); * } * * @example * // Click the mouse near the red dot in the top-left corner * // and drag to change the curve's shape. * * let x2 = 10; * let y2 = 10; * let isChanging = false; * * function setup() { * createCanvas(100, 100); * * describe( * 'A gray square with three curves. A black s-curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.' * ); * } * * function draw() { * background(200); * * // Draw the anchor points in black. * stroke(0); * strokeWeight(5); * point(85, 20); * point(15, 80); * * // Draw the control points in red. * stroke(255, 0, 0); * point(x2, y2); * point(90, 90); * * // Draw a black bezier curve. * noFill(); * stroke(0); * strokeWeight(1); * bezier(85, 20, x2, y2, 90, 90, 15, 80); * * // Draw red lines from the anchor points to the control points. * stroke(255, 0, 0); * line(85, 20, x2, y2); * line(15, 80, 90, 90); * } * * // Start changing the first control point if the user clicks near it. * function mousePressed() { * if (dist(mouseX, mouseY, x2, y2) < 20) { * isChanging = true; * } * } * * // Stop changing the first control point when the user releases the mouse. * function mouseReleased() { * isChanging = false; * } * * // Update the first control point while the user drags the mouse. * function mouseDragged() { * if (isChanging === true) { * x2 = mouseX; * y2 = mouseY; * } * } * * @example * function setup() { * createCanvas(100, 100); * * background('skyblue'); * * // Draw the red balloon. * fill('red'); * bezier(50, 60, 5, 15, 95, 15, 50, 60); * * // Draw the balloon string. * line(50, 60, 50, 80); * * describe('A red balloon in a blue sky.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A red balloon in a blue sky. The balloon rotates slowly, revealing that it is flat.'); * } * * function draw() { * background('skyblue'); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Draw the red balloon. * fill('red'); * bezier(0, 0, 0, -45, -45, 0, 45, -45, 0, 0, 0, 0); * * // Draw the balloon string. * line(0, 0, 0, 0, 20, 0); * } */ /** * @method bezier * @param {Number} x1 * @param {Number} y1 * @param {Number} z1 z-coordinate of the first anchor point. * @param {Number} x2 * @param {Number} y2 * @param {Number} z2 z-coordinate of the first control point. * @param {Number} x3 * @param {Number} y3 * @param {Number} z3 z-coordinate of the second control point. * @param {Number} x4 * @param {Number} y4 * @param {Number} z4 z-coordinate of the second anchor point. * @chainable */ fn.bezier = function(...args) { // p5._validateParameters('bezier', args); // if the current stroke and fill settings wouldn't result in something // visible, exit immediately if ( !this._renderer.states.strokeColor && !this._renderer.states.fillColor ) { return this; } this._renderer.bezier(...args); return this; }; /** * Calculates coordinates along a Bézier curve using interpolation. * * `bezierPoint()` calculates coordinates along a Bézier curve using the * anchor and control points. It expects points in the same order as the * bezier() function. `bezierPoint()` works one axis * at a time. Passing the anchor and control points' x-coordinates will * calculate the x-coordinate of a point on the curve. Passing the anchor and * control points' y-coordinates will calculate the y-coordinate of a point on * the curve. * * The first parameter, `a`, is the coordinate of the first anchor point. * * The second and third parameters, `b` and `c`, are the coordinates of the * control points. * * The fourth parameter, `d`, is the coordinate of the last anchor point. * * The fifth parameter, `t`, is the amount to interpolate along the curve. 0 * is the first anchor point, 1 is the second anchor point, and 0.5 is halfway * between them. * * @method bezierPoint * @param {Number} a coordinate of first anchor point. * @param {Number} b coordinate of first control point. * @param {Number} c coordinate of second control point. * @param {Number} d coordinate of second anchor point. * @param {Number} t amount to interpolate between 0 and 1. * @return {Number} coordinate of the point on the curve. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the coordinates for the curve's anchor and control points. * let x1 = 85; * let x2 = 10; * let x3 = 90; * let x4 = 15; * let y1 = 20; * let y2 = 10; * let y3 = 90; * let y4 = 80; * * // Style the curve. * noFill(); * * // Draw the curve. * bezier(x1, y1, x2, y2, x3, y3, x4, y4); * * // Draw circles along the curve's path. * fill(255); * * // Top-right. * let x = bezierPoint(x1, x2, x3, x4, 0); * let y = bezierPoint(y1, y2, y3, y4, 0); * circle(x, y, 5); * * // Center. * x = bezierPoint(x1, x2, x3, x4, 0.5); * y = bezierPoint(y1, y2, y3, y4, 0.5); * circle(x, y, 5); * * // Bottom-left. * x = bezierPoint(x1, x2, x3, x4, 1); * y = bezierPoint(y1, y2, y3, y4, 1); * circle(x, y, 5); * * describe('A black s-curve on a gray square. The endpoints and center of the curve are marked with white circles.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A black s-curve on a gray square. A white circle moves back and forth along the curve.'); * } * * function draw() { * background(200); * * // Set the coordinates for the curve's anchor and control points. * let x1 = 85; * let x2 = 10; * let x3 = 90; * let x4 = 15; * let y1 = 20; * let y2 = 10; * let y3 = 90; * let y4 = 80; * * // Draw the curve. * noFill(); * bezier(x1, y1, x2, y2, x3, y3, x4, y4); * * // Calculate the circle's coordinates. * let t = 0.5 * sin(frameCount * 0.01) + 0.5; * let x = bezierPoint(x1, x2, x3, x4, t); * let y = bezierPoint(y1, y2, y3, y4, t); * * // Draw the circle. * fill(255); * circle(x, y, 5); * } */ fn.bezierPoint = function(a, b, c, d, t) { // p5._validateParameters('bezierPoint', arguments); const adjustedT = 1 - t; return ( Math.pow(adjustedT, 3) * a + 3 * Math.pow(adjustedT, 2) * t * b + 3 * adjustedT * Math.pow(t, 2) * c + Math.pow(t, 3) * d ); }; /** * Calculates coordinates along a line that's tangent to a Bézier curve. * * Tangent lines skim the surface of a curve. A tangent line's slope equals * the curve's slope at the point where it intersects. * * `bezierTangent()` calculates coordinates along a tangent line using the * Bézier curve's anchor and control points. It expects points in the same * order as the bezier() function. `bezierTangent()` * works one axis at a time. Passing the anchor and control points' * x-coordinates will calculate the x-coordinate of a point on the tangent * line. Passing the anchor and control points' y-coordinates will calculate * the y-coordinate of a point on the tangent line. * * The first parameter, `a`, is the coordinate of the first anchor point. * * The second and third parameters, `b` and `c`, are the coordinates of the * control points. * * The fourth parameter, `d`, is the coordinate of the last anchor point. * * The fifth parameter, `t`, is the amount to interpolate along the curve. 0 * is the first anchor point, 1 is the second anchor point, and 0.5 is halfway * between them. * * @method bezierTangent * @param {Number} a coordinate of first anchor point. * @param {Number} b coordinate of first control point. * @param {Number} c coordinate of second control point. * @param {Number} d coordinate of second anchor point. * @param {Number} t amount to interpolate between 0 and 1. * @return {Number} coordinate of a point on the tangent line. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the coordinates for the curve's anchor and control points. * let x1 = 85; * let x2 = 10; * let x3 = 90; * let x4 = 15; * let y1 = 20; * let y2 = 10; * let y3 = 90; * let y4 = 80; * * // Style the curve. * noFill(); * * // Draw the curve. * bezier(x1, y1, x2, y2, x3, y3, x4, y4); * * // Draw tangents along the curve's path. * fill(255); * * // Top-right circle. * stroke(0); * let x = bezierPoint(x1, x2, x3, x4, 0); * let y = bezierPoint(y1, y2, y3, y4, 0); * circle(x, y, 5); * * // Top-right tangent line. * // Scale the tangent point to draw a shorter line. * stroke(255, 0, 0); * let tx = 0.1 * bezierTangent(x1, x2, x3, x4, 0); * let ty = 0.1 * bezierTangent(y1, y2, y3, y4, 0); * line(x + tx, y + ty, x - tx, y - ty); * * // Center circle. * stroke(0); * x = bezierPoint(x1, x2, x3, x4, 0.5); * y = bezierPoint(y1, y2, y3, y4, 0.5); * circle(x, y, 5); * * // Center tangent line. * // Scale the tangent point to draw a shorter line. * stroke(255, 0, 0); * tx = 0.1 * bezierTangent(x1, x2, x3, x4, 0.5); * ty = 0.1 * bezierTangent(y1, y2, y3, y4, 0.5); * line(x + tx, y + ty, x - tx, y - ty); * * // Bottom-left circle. * stroke(0); * x = bezierPoint(x1, x2, x3, x4, 1); * y = bezierPoint(y1, y2, y3, y4, 1); * circle(x, y, 5); * * // Bottom-left tangent. * // Scale the tangent point to draw a shorter line. * stroke(255, 0, 0); * tx = 0.1 * bezierTangent(x1, x2, x3, x4, 1); * ty = 0.1 * bezierTangent(y1, y2, y3, y4, 1); * line(x + tx, y + ty, x - tx, y - ty); * * describe( * 'A black s-curve on a gray square. The endpoints and center of the curve are marked with white circles. Red tangent lines extend from the white circles.' * ); * } */ fn.bezierTangent = function(a, b, c, d, t) { // p5._validateParameters('bezierTangent', arguments); const adjustedT = 1 - t; return ( 3 * d * Math.pow(t, 2) - 3 * c * Math.pow(t, 2) + 6 * c * adjustedT * t - 6 * b * adjustedT * t + 3 * b * Math.pow(adjustedT, 2) - 3 * a * Math.pow(adjustedT, 2) ); }; /** * Draws a curve using a Catmull-Rom spline. * * Spline curves can form shapes and curves that slope gently. They’re like * cables that are attached to a set of points. By default (`ends: INCLUDE`), * the curve passes through all four points you provide, in order * `p0(x1,y1)` -> `p1(x2,y2)` -> `p2(x3,y3)` -> `p3(x4,y4)`. Think of them as * points on a curve. If you switch to `ends: EXCLUDE`, p0 and p3 act * like control points and only the middle span `p1->p2` is drawn. * * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of * `spline()` has twelve arguments because each point has x-, y-, and * z-coordinates. * * @method spline * @param {Number} x1 x-coordinate of point p0. * @param {Number} y1 y-coordinate of point p0. * @param {Number} x2 x-coordinate of point p1. * @param {Number} y2 y-coordinate of point p1. * @param {Number} x3 x-coordinate of point p2. * @param {Number} y3 y-coordinate of point p2. * @param {Number} x4 x-coordinate of point p3. * @param {Number} y4 y-coordinate of point p3. * @chainable * * @example * function setup() { * createCanvas(200, 200); * background(240); * noFill(); * * stroke(0); * strokeWeight(2); * spline(40, 60, 100, 40, 120, 120, 60, 140); * * strokeWeight(5); * point(40, 60); * point(100, 40); * point(120, 120); * point(60, 140); * * describe('A black spline passes smoothly through four points'); * } * * @example * function setup() { * createCanvas(200, 200); * background(245); * * // Ensure the curve includes both end spans p0->p1 and p2->p3 * splineProperty('ends', INCLUDE); * * // Control / anchor points * const p0 = createVector(30, 160); * const p1 = createVector(60, 40); * const p2 = createVector(140, 40); * const p3 = createVector(170, 160); * * // Draw the spline that passes through ALL four points (INCLUDE) * noFill(); * stroke(0); * strokeWeight(2); * spline(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); * * // Draw markers + labels * fill(255); * stroke(0); * const r = 6; * circle(p0.x, p0.y, r); * circle(p1.x, p1.y, r); * circle(p2.x, p2.y, r); * circle(p3.x, p3.y, r); * * noStroke(); * fill(0); * text('p0', p0.x - 14, p0.y + 14); * text('p1', p1.x - 14, p1.y - 8); * text('p2', p2.x + 4, p2.y - 8); * text('p3', p3.x + 4, p3.y + 14); * * describe('A black Catmull-Rom spline passes through p0, p1, p2, p3 with endpoints included.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn. * splineProperty('ends', EXCLUDE); * * // Draw a black spline curve. * noFill(); * strokeWeight(1); * stroke(0); * spline(5, 26, 73, 24, 73, 61, 15, 65); * * // Draw red spline curves from the points. * stroke(255, 0, 0); * spline(5, 26, 5, 26, 73, 24, 73, 61); * spline(73, 24, 73, 61, 15, 65, 15, 65); * * // Draw the points in black. * strokeWeight(5); * stroke(0); * point(73, 24); * point(73, 61); * * // Draw the points in red. * stroke(255, 0, 0); * point(5, 26); * point(15, 65); * * describe( * 'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.' * ); * } * * @example * let x1 = 5; * let y1 = 26; * let isChanging = false; * * function setup() { * createCanvas(100, 100); * * describe( * 'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.' * ); * } * * function draw() { * background(200); * * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn. * splineProperty('ends', EXCLUDE); * * // Draw a black spline curve. * noFill(); * strokeWeight(1); * stroke(0); * spline(x1, y1, 73, 24, 73, 61, 15, 65); * * // Draw red spline curves from the points. * stroke(255, 0, 0); * spline(x1, y1, x1, y1, 73, 24, 73, 61); * spline(73, 24, 73, 61, 15, 65, 15, 65); * * // Draw the anchor points in black. * strokeWeight(5); * stroke(0); * point(73, 24); * point(73, 61); * * // Draw the points in red. * stroke(255, 0, 0); * point(x1, y1); * point(15, 65); * } * * // Start changing the first point if the user clicks near it. * function mousePressed() { * if (dist(mouseX, mouseY, x1, y1) < 20) { * isChanging = true; * } * } * * // Stop changing the first point when the user releases the mouse. * function mouseReleased() { * isChanging = false; * } * * // Update the first point while the user drags the mouse. * function mouseDragged() { * if (isChanging === true) { * x1 = mouseX; * y1 = mouseY; * } * } * * @example * function setup() { * createCanvas(100, 100); * * background('skyblue'); * * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn. * splineProperty('ends', EXCLUDE); * * // Draw the red balloon. * fill('red'); * spline(-150, 275, 50, 60, 50, 60, 250, 275); * * // Draw the balloon string. * line(50, 60, 50, 80); * * describe('A red balloon in a blue sky.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A red balloon in a blue sky.'); * } * * function draw() { * background('skyblue'); * * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn. * splineProperty('ends', EXCLUDE); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Draw the red balloon. * fill('red'); * spline(-200, 225, 0, 0, 10, 0, 0, 10, 0, 200, 225, 0); * * // Draw the balloon string. * line(0, 10, 0, 0, 30, 0); * } */ /** * @method spline * @param {Number} x1 * @param {Number} y1 * @param {Number} z1 z-coordinate of point p0. * @param {Number} x2 * @param {Number} y2 * @param {Number} z2 z-coordinate of point p1. * @param {Number} x3 * @param {Number} y3 * @param {Number} z3 z-coordinate of point p2. * @param {Number} x4 * @param {Number} y4 * @param {Number} z4 z-coordinate of point p3. * @chainable */ fn.spline = function(...args) { if ( !this._renderer.states.strokeColor && !this._renderer.states.fillColor ) { return this; } this._renderer.spline(...args); return this; }; /** * Calculates coordinates along a spline curve using interpolation. * * `splinePoint()` calculates coordinates along a spline curve using four * points p0, p1, p2, p3. It expects points in the same order as the * spline() function. `splinePoint()` works one axis * at a time. Passing the points' x-coordinates will * calculate the x-coordinate of a point on the curve. Passing the * points' y-coordinates will calculate the y-coordinate of a point on * the curve. * * The first parameter, `a`, is the coordinate of point p0. * * The second and third parameters, `b` and `c`, are the coordinates of * points p1 and p2. * * The fourth parameter, `d`, is the coordinate of point p3. * * The fifth parameter, `t`, is the amount to interpolate along the span * from p1 to p2. `t = 0` is p1, `t = 1` is p2, and `t = 0.5` is halfway * between them. * * @method splinePoint * @param {Number} a coordinate of point p0. * @param {Number} b coordinate of point p1. * @param {Number} c coordinate of point p2. * @param {Number} d coordinate of point p3. * @param {Number} t amount to interpolate between 0 and 1. * @return {Number} coordinate of a point on the curve. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * * // Set the coordinates for the curve's four points (p0, p1, p2, p3). * let x1 = 5; * let y1 = 26; * let x2 = 73; * let y2 = 24; * let x3 = 73; * let y3 = 61; * let x4 = 15; * let y4 = 65; * * // Draw the curve. * noFill(); * spline(x1, y1, x2, y2, x3, y3, x4, y4); * * // Draw circles along the curve's path. * fill(255); * * // Top. * let x = splinePoint(x1, x2, x3, x4, 0); * let y = splinePoint(y1, y2, y3, y4, 0); * circle(x, y, 5); * * // Center. * x = splinePoint(x1, x2, x3, x4, 0.5); * y = splinePoint(y1, y2, y3, y4, 0.5); * circle(x, y, 5); * * // Bottom. * x = splinePoint(x1, x2, x3, x4, 1); * y = splinePoint(y1, y2, y3, y4, 1); * circle(x, y, 5); * * describe('A black curve on a gray square. The endpoints and center of the curve are marked with white circles.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A black curve on a gray square. A white circle moves back and forth along the curve.'); * } * * function draw() { * background(200); * * // Set the coordinates for the curve's four points (p0, p1, p2, p3). * let x1 = 5; * let y1 = 26; * let x2 = 73; * let y2 = 24; * let x3 = 73; * let y3 = 61; * let x4 = 15; * let y4 = 65; * * // Draw the curve. * noFill(); * spline(x1, y1, x2, y2, x3, y3, x4, y4); * * // Calculate the circle's coordinates. * let t = 0.5 * sin(frameCount * 0.01) + 0.5; * let x = splinePoint(x1, x2, x3, x4, t); * let y = splinePoint(y1, y2, y3, y4, t); * * // Draw the circle. * fill(255); * circle(x, y, 5); * } * * @example * let p0, p1, p2, p3; * * function setup() { * createCanvas(200, 200); * splineProperty('ends', INCLUDE); // make endpoints part of the curve * * // Four points forming a gentle arch * p0 = createVector(30, 160); * p1 = createVector(60, 50); * p2 = createVector(140, 50); * p3 = createVector(170, 160); * * describe('Black spline through p0–p3. A red dot marks the location at parameter t on p1->p2 using splinePoint.'); * } * * function draw() { * background(245); * * // Draw the spline for context * noFill(); * stroke(0); * strokeWeight(2); * spline(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); * * // Map mouse X to t in [0, 1] (span p1->p2) * let t = constrain(map(mouseX, 0, width, 0, 1), 0, 1); * * // Evaluate the curve point by axis (splinePoint works one axis at a time) * let x = splinePoint(p0.x, p1.x, p2.x, p3.x, t); * let y = splinePoint(p0.y, p1.y, p2.y, p3.y, t); * * // Marker at the evaluated position * noStroke(); * fill('red'); * circle(x, y, 8); * * // Draw control/anchor points * stroke(0); * strokeWeight(1); * fill(255); * const r = 6; * circle(p0.x, p0.y, r); * circle(p1.x, p1.y, r); * circle(p2.x, p2.y, r); * circle(p3.x, p3.y, r); * * // Labels + UI hint * noStroke(); * fill(20); * textSize(10); * text('p0', p0.x - 12, p0.y + 14); * text('p1', p1.x - 12, p1.y - 8); * text('p2', p2.x + 4, p2.y - 8); * text('p3', p3.x + 4, p3.y + 14); * text('t = ' + nf(t, 1, 2) + ' (p1→p2)', 8, 16); * } */ fn.splinePoint = function(a, b, c, d, t) { const s = this._renderer.states.splineProperties.tightness, t3 = t * t * t, t2 = t * t, f1 = (s - 1) / 2 * t3 + (1 - s) * t2 + (s - 1) / 2 * t, f2 = (s + 3) / 2 * t3 + (-5 - s) / 2 * t2 + 1.0, f3 = (-3 - s) / 2 * t3 + (s + 2) * t2 + (1 - s) / 2 * t, f4 = (1 - s) / 2 * t3 + (s - 1) / 2 * t2; return a * f1 + b * f2 + c * f3 + d * f4; }; /** * Calculates coordinates along a line that's tangent to a spline curve. * * Tangent lines skim the surface of a curve. A tangent line's slope equals * the curve's slope at the point where it intersects. * * `splineTangent()` calculates coordinates along a tangent line using four * points p0, p1, p2, p3. It expects points in the same order as the * spline() function. `splineTangent()` works one * axis at a time. Passing the points' x-coordinates returns the x-component of * the tangent vector; passing the points' y-coordinates returns the y-component. * The first parameter, `a`, is the coordinate of point p0. * * The second and third parameters, `b` and `c`, are the coordinates of * points p1 and p2. * * The fourth parameter, `d`, is the coordinate of point p3. * * The fifth parameter, `t`, is the amount to interpolate along the span * from p1 to p2. `t = 0` is p1, `t = 1` is p2, and `t = 0.5` is halfway * between them. * * @method splineTangent * @param {Number} a coordinate of point p0. * @param {Number} b coordinate of point p1. * @param {Number} c coordinate of point p2. * @param {Number} d coordinate of point p3. * @param {Number} t amount to interpolate between 0 and 1. * @return {Number} coordinate of a point on the tangent line. * * @example * function setup() { * createCanvas(120, 120); * describe('A black spline on a gray canvas. A red dot moves along the curve on its own. A short line shows the tangent direction at the dot.'); * } * * function draw() { * background(240); * * const x1 = 15, y1 = 40; * const x2 = 90, y2 = 25; * const x3 = 95, y3 = 95; * const x4 = 30, y4 = 110; * * noFill(); * stroke(0); * strokeWeight(2); * spline(x1, y1, x2, y2, x3, y3, x4, y4); * * const t = 0.5 + 0.5 * sin(frameCount * 0.03); * * const px = splinePoint(x1, x2, x3, x4, t); * const py = splinePoint(y1, y2, y3, y4, t); * * let tx = splineTangent(x1, x2, x3, x4, t); * let ty = splineTangent(y1, y2, y3, y4, t); * * const m = Math.hypot(tx, ty) || 1; * tx = (tx / m) * 16; * ty = (ty / m) * 16; * * stroke(0); * strokeWeight(2); * line(px, py, px + tx, py + ty); * * noStroke(); * fill('red'); * circle(px, py, 7); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the coordinates for the curve's four points (p0, p1, p2, p3). * let x1 = 5; * let y1 = 26; * let x2 = 73; * let y2 = 24; * let x3 = 73; * let y3 = 61; * let x4 = 15; * let y4 = 65; * * // Draw the curve. * noFill(); * spline(x1, y1, x2, y2, x3, y3, x4, y4); * * // Draw tangents along the curve's path. * fill(255); * * // Top circle. * stroke(0); * let x = splinePoint(x1, x2, x3, x4, 0); * let y = splinePoint(y1, y2, y3, y4, 0); * circle(x, y, 5); * * // Top tangent line. * // Scale the tangent point to draw a shorter line. * stroke(255, 0, 0); * let tx = 0.2 * splineTangent(x1, x2, x3, x4, 0); * let ty = 0.2 * splineTangent(y1, y2, y3, y4, 0); * line(x + tx, y + ty, x - tx, y - ty); * * // Center circle. * stroke(0); * x = splinePoint(x1, x2, x3, x4, 0.5); * y = splinePoint(y1, y2, y3, y4, 0.5); * circle(x, y, 5); * * // Center tangent line. * // Scale the tangent point to draw a shorter line. * stroke(255, 0, 0); * tx = 0.2 * splineTangent(x1, x2, x3, x4, 0.5); * ty = 0.2 * splineTangent(y1, y2, y3, y4, 0.5); * line(x + tx, y + ty, x - tx, y - ty); * * // Bottom circle. * stroke(0); * x = splinePoint(x1, x2, x3, x4, 1); * y = splinePoint(y1, y2, y3, y4, 1); * circle(x, y, 5); * * // Bottom tangent line. * // Scale the tangent point to draw a shorter line. * stroke(255, 0, 0); * tx = 0.2 * splineTangent(x1, x2, x3, x4, 1); * ty = 0.2 * splineTangent(y1, y2, y3, y4, 1); * line(x + tx, y + ty, x - tx, y - ty); * * describe( * 'A black curve on a gray square. A white circle moves back and forth along the curve.' * ); * } */ fn.splineTangent = function(a, b, c, d, t) { const s = this._renderer.states.splineProperties.tightness, tt3 = t * t * 3, t2 = t * 2, f1 = (s - 1) / 2 * tt3 + (1 - s) * t2 + (s - 1) / 2, f2 = (s + 3) / 2 * tt3 + (-5 - s) / 2 * t2, f3 = (-3 - s) / 2 * tt3 + (s + 2) * t2 + (1 - s) / 2, f4 = (1 - s) / 2 * tt3 + (s - 1) / 2 * t2; return a * f1 + b * f2 + c * f3 + d * f4; }; } if(typeof p5 !== 'undefined'){ curves(p5, p5.prototype); } /** * @module Shape * @submodule Custom Shapes * @for p5 * @requires core * @requires constants */ function vertex(p5, fn){ /** * Begins adding vertices to a custom shape. * * The `beginShape()` and endShape() functions * allow for creating custom shapes in 2D or 3D. `beginShape()` begins adding * vertices to a custom shape and endShape() stops * adding them. * * The parameter, `kind`, sets the kind of shape to make. The available kinds are: * * - `PATH` (the default) to draw shapes by tracing out the path along their edges. * - `POINTS` to draw a series of points. * - `LINES` to draw a series of unconnected line segments. * - `TRIANGLES` to draw a series of separate triangles. * - `TRIANGLE_FAN` to draw a series of connected triangles sharing the first vertex in a fan-like fashion. * - `TRIANGLE_STRIP` to draw a series of connected triangles in strip fashion. * - `QUADS` to draw a series of separate quadrilaterals (quads). * - `QUAD_STRIP` to draw quad strip using adjacent edges to form the next quad. * * After calling `beginShape()`, shapes can be built by calling * vertex(), * bezierVertex(), and/or * splineVertex(). Calling * endShape() will stop adding vertices to the * shape. Each shape will be outlined with the current stroke color and filled * with the current fill color. * * Transformations such as translate(), * rotate(), and * scale() don't work between `beginShape()` and * endShape(). It's also not possible to use * other shapes, such as ellipse() or * rect(), between `beginShape()` and * endShape(). * * @method beginShape * @param {(POINTS|LINES|TRIANGLES|TRIANGLE_FAN|TRIANGLE_STRIP|QUADS|QUAD_STRIP|PATH)} [kind=PATH] either POINTS, LINES, TRIANGLES, TRIANGLE_FAN * TRIANGLE_STRIP, QUADS, QUAD_STRIP or PATH. Defaults to PATH. * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * beginShape(); * * // Add vertices. * vertex(30, 20); * vertex(85, 20); * vertex(85, 75); * vertex(30, 75); * * // Stop drawing the shape. * endShape(CLOSE); * * describe('A white square on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * // Only draw the vertices (points). * beginShape(POINTS); * * // Add vertices. * vertex(30, 20); * vertex(85, 20); * vertex(85, 75); * vertex(30, 75); * * // Stop drawing the shape. * endShape(); * * describe('Four black dots that form a square are drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * // Only draw lines between alternating pairs of vertices. * beginShape(LINES); * * // Add vertices. * vertex(30, 20); * vertex(85, 20); * vertex(85, 75); * vertex(30, 75); * * // Stop drawing the shape. * endShape(); * * describe('Two horizontal black lines on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the shape. * noFill(); * * // Start drawing the shape. * beginShape(); * * // Add vertices. * vertex(30, 20); * vertex(85, 20); * vertex(85, 75); * vertex(30, 75); * * // Stop drawing the shape. * endShape(); * * describe('Three black lines form a sideways U shape on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the shape. * noFill(); * * // Start drawing the shape. * beginShape(); * * // Add vertices. * vertex(30, 20); * vertex(85, 20); * vertex(85, 75); * vertex(30, 75); * * // Stop drawing the shape. * // Connect the first and last vertices. * endShape(CLOSE); * * describe('A black outline of a square drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * // Draw a series of triangles. * beginShape(TRIANGLES); * * // Left triangle. * vertex(30, 75); * vertex(40, 20); * vertex(50, 75); * * // Right triangle. * vertex(60, 20); * vertex(70, 75); * vertex(80, 20); * * // Stop drawing the shape. * endShape(); * * describe('Two white triangles drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * // Draw a series of triangles. * beginShape(TRIANGLE_STRIP); * * // Add vertices. * vertex(30, 75); * vertex(40, 20); * vertex(50, 75); * vertex(60, 20); * vertex(70, 75); * vertex(80, 20); * vertex(90, 75); * * // Stop drawing the shape. * endShape(); * * describe('Five white triangles that are interleaved drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * // Draw a series of triangles that share their first vertex. * beginShape(TRIANGLE_FAN); * * // Add vertices. * vertex(57.5, 50); * vertex(57.5, 15); * vertex(92, 50); * vertex(57.5, 85); * vertex(22, 50); * vertex(57.5, 15); * * // Stop drawing the shape. * endShape(); * * describe('Four white triangles form a square are drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * // Draw a series of quadrilaterals. * beginShape(QUADS); * * // Left rectangle. * vertex(30, 20); * vertex(30, 75); * vertex(50, 75); * vertex(50, 20); * * // Right rectangle. * vertex(65, 20); * vertex(65, 75); * vertex(85, 75); * vertex(85, 20); * * // Stop drawing the shape. * endShape(); * * describe('Two white rectangles drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * // Draw a series of quadrilaterals. * beginShape(QUAD_STRIP); * * // Add vertices. * vertex(30, 20); * vertex(30, 75); * vertex(50, 20); * vertex(50, 75); * vertex(65, 20); * vertex(65, 75); * vertex(85, 20); * vertex(85, 75); * * // Stop drawing the shape. * endShape(); * * describe('Three white rectangles that share edges are drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Start drawing the shape. * // Draw a series of quadrilaterals. * beginShape(PATH); * * // Add the vertices. * vertex(-30, -30, 0); * vertex(30, -30, 0); * vertex(30, -10, 0); * vertex(-10, -10, 0); * vertex(-10, 10, 0); * vertex(30, 10, 0); * vertex(30, 30, 0); * vertex(-30, 30, 0); * * // Stop drawing the shape. * // Connect the first and last vertices. * endShape(CLOSE); * * describe('A blocky C shape drawn in white on a gray background.'); * } * * @example * // Click and drag with the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A blocky C shape drawn in red, blue, and green on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Start drawing the shape. * // Draw a series of quadrilaterals. * beginShape(PATH); * * // Add the vertices. * fill('red'); * stroke('red'); * vertex(-30, -30, 0); * vertex(30, -30, 0); * vertex(30, -10, 0); * fill('green'); * stroke('green'); * vertex(-10, -10, 0); * vertex(-10, 10, 0); * vertex(30, 10, 0); * fill('blue'); * stroke('blue'); * vertex(30, 30, 0); * vertex(-30, 30, 0); * * // Stop drawing the shape. * // Connect the first and last vertices. * endShape(CLOSE); * } */ fn.beginShape = function(kind) { // p5._validateParameters('beginShape', arguments); this._renderer.beginShape(...arguments); }; /** * Adds a Bézier curve segment to a custom shape. * * `bezierVertex()` adds a curved segment to custom shapes. The Bézier curves * it creates are defined like those made by the * bezier() function. `bezierVertex()` must be * called between the * beginShape() and * endShape() functions. * Bézier need a starting point. Building a shape * only with Bézier curves needs one initial * call to bezierVertex(), before * a number of `bezierVertex()` calls that is a multiple of the parameter * set by bezierOrder(...) (default 3). * But shapes can mix different types of vertices, so if there * are some previous vertices, then the initial anchor is not needed, * only the multiples of 3 (or the Bézier order) calls to * `bezierVertex` for each curve. * * Each curve of order 3 requires three calls to `bezierVertex`, so * 2 curves would need 7 calls to `bezierVertex()`: * (1 one initial anchor point, two sets of 3 curves describing the curves) * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2. * * Bézier curves can also be drawn in 3D using WebGL mode. * * Note: `bezierVertex()` won’t work when an argument is passed to * beginShape(). * * @method bezierVertex * @param {Number} x x-coordinate of the first control point. * @param {Number} y y-coordinate of the first control point. * @param {Number} [u] * @param {Number} [v] * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the shape. * noFill(); * * // Start drawing the shape. * beginShape(); * * // Add the first anchor point. * bezierVertex(30, 20); * * // Add the Bézier vertex. * bezierVertex(80, 0); * bezierVertex(80, 75); * bezierVertex(30, 75); * * // Stop drawing the shape. * endShape(); * * describe('A black C curve on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the anchor points in black. * stroke(0); * strokeWeight(5); * point(30, 20); * point(30, 75); * * // Draw the control points in red. * stroke(255, 0, 0); * point(80, 0); * point(80, 75); * * // Style the shape. * noFill(); * stroke(0); * strokeWeight(1); * * // Start drawing the shape. * beginShape(); * * // Add the first anchor point. * bezierVertex(30, 20); * * // Add the Bézier vertex. * bezierVertex(80, 0); * bezierVertex(80, 75); * bezierVertex(30, 75); * * // Stop drawing the shape. * endShape(); * * // Draw red lines from the anchor points to the control points. * stroke(255, 0, 0); * line(30, 20, 80, 0); * line(30, 75, 80, 75); * * describe( * 'A gray square with three curves. A black curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.' * ); * } * * @example * // Click the mouse near the red dot in the top-right corner * // and drag to change the curve's shape. * * let x2 = 80; * let y2 = 0; * let isChanging = false; * * function setup() { * createCanvas(100, 100); * * describe( * 'A gray square with three curves. A black curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.' * ); * } * * function draw() { * background(200); * * // Draw the anchor points in black. * stroke(0); * strokeWeight(5); * point(30, 20); * point(30, 75); * * // Draw the control points in red. * stroke(255, 0, 0); * point(x2, y2); * point(80, 75); * * // Style the shape. * noFill(); * stroke(0); * strokeWeight(1); * * // Start drawing the shape. * beginShape(); * * // Add the first anchor point. * bezierVertex(30, 20); * * // Add the Bézier vertex. * bezierVertex(x2, y2); * bezierVertex(80, 75); * bezierVertex(30, 75); * * // Stop drawing the shape. * endShape(); * * // Draw red lines from the anchor points to the control points. * stroke(255, 0, 0); * line(30, 20, x2, y2); * line(30, 75, 80, 75); * } * * // Start changing the first control point if the user clicks near it. * function mousePressed() { * if (dist(mouseX, mouseY, x2, y2) < 20) { * isChanging = true; * } * } * * // Stop changing the first control point when the user releases the mouse. * function mouseReleased() { * isChanging = false; * } * * // Update the first control point while the user drags the mouse. * function mouseDragged() { * if (isChanging === true) { * x2 = mouseX; * y2 = mouseY; * } * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Start drawing the shape. * beginShape(); * * // Add the first anchor point. * bezierVertex(30, 20); * * // Add the Bézier vertices. * bezierVertex(80, 0); * bezierVertex(80, 75); * bezierVertex(30, 75); * * bezierVertex(50, 80); * bezierVertex(60, 25); * bezierVertex(30, 20); * * // Stop drawing the shape. * endShape(); * * describe('A crescent moon shape drawn in white on a gray background.'); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A crescent moon shape drawn in white on a blue background. When the user drags the mouse, the scene rotates and a second moon is revealed.'); * } * * function draw() { * background('midnightblue'); * * // Enable orbiting with the mouse. * orbitControl(); * * // Style the moons. * noStroke(); * fill('lemonchiffon'); * * // Draw the first moon. * beginShape(); * bezierVertex(-20, -30, 0); * * bezierVertex(30, -50, 0); * bezierVertex(30, 25, 0); * bezierVertex(-20, 25, 0); * * bezierVertex(0, 30, 0); * bezierVertex(10, -25, 0); * bezierVertex(-20, -30, 0); * endShape(); * * // Draw the second moon. * beginShape(); * * bezierVertex(-20, -30, -20); * * bezierVertex(30, -50, -20); * bezierVertex(30, 25, -20); * bezierVertex(-20, 25, -20); * * bezierVertex(0, 30, -20); * bezierVertex(10, -25, -20); * bezierVertex(-20, -30, -20); * * endShape(); * } */ /** * @method bezierVertex * @param {Number} x * @param {Number} y * @param {Number} z * @param {Number} [u] * @param {Number} [v] */ fn.bezierVertex = function(...args) { this._renderer.bezierVertex(...args); }; /** * Concludes the vertices of a custom shape. * * The beginShape() and `endShape()` functions * allow for creating custom shapes in 2D or 3D. * beginShape() begins adding vertices to a * custom shape and `endShape()` stops adding them. * * The first parameter, `mode`, is optional. By default, the first and last * vertices of a shape aren't connected. If the constant `CLOSE` is passed, as * in `endShape(CLOSE)`, then the first and last vertices will be connected. * When CLOSE mode is used for splines (with `splineVeertex()`), the shape is ended smoothly. * * * The second parameter, `count`, is also optional. In WebGL mode, it’s more * efficient to draw many copies of the same shape using a technique called * instancing. * The `count` parameter tells WebGL mode how many copies to draw. For * example, calling `endShape(CLOSE, 400)` after drawing a custom shape will * make it efficient to draw 400 copies. This feature requires * writing a custom shader. * * After calling beginShape(), shapes can be * built by calling vertex(), * bezierVertex() and/or * splineVertex(). Calling * `endShape()` will stop adding vertices to the * shape. Each shape will be outlined with the current stroke color and filled * with the current fill color. * * Transformations such as translate(), * rotate(), and * scale() don't work between * beginShape() and `endShape()`. It's also not * possible to use other shapes, such as ellipse() or * rect(), between * beginShape() and `endShape()`. * * @method endShape * @param {CLOSE} [mode] use CLOSE to close the shape * @param {Integer} [count] number of times you want to draw/instance the shape (for WebGL mode). * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the shapes. * noFill(); * * // Left triangle. * beginShape(); * vertex(20, 20); * vertex(45, 20); * vertex(45, 80); * endShape(CLOSE); * * // Right triangle. * beginShape(); * vertex(50, 20); * vertex(75, 20); * vertex(75, 80); * endShape(); * * describe( * 'Two sets of black lines drawn on a gray background. The three lines on the left form a right triangle. The two lines on the right form a right angle.' * ); * } * * @example * function setup() { * createCanvas(200, 100); * * background(240); * * noFill(); * stroke(0); * * // Open shape (left) * beginShape(); * vertex(20, 20); * vertex(80, 20); * vertex(80, 80); * endShape(); // Not closed * * // Closed shape (right) * beginShape(); * vertex(120, 20); * vertex(180, 20); * vertex(180, 80); * endShape(CLOSE); // Closed * * describe( * 'Two right-angled shapes on a light gray background. The left shape is open with three lines. The right shape is closed, forming a triangle.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * background(200); * * beginShape(); * * splineVertex(32, 91); * splineVertex(21, 17); * splineVertex(68, 19); * splineVertex(82, 91); * * endShape(CLOSE); * * describe( * 'A curvy four-sided slightly lopsided blob.' * ); * } * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = `#version 300 es * * precision mediump float; * * in vec3 aPosition; * flat out int instanceID; * * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * void main() { * * // Copy the instance ID to the fragment shader. * instanceID = gl_InstanceID; * vec4 positionVec4 = vec4(aPosition, 1.0); * * // gl_InstanceID represents a numeric value for each instance. * // Using gl_InstanceID allows us to move each instance separately. * // Here we move each instance horizontally by ID * 23. * float xOffset = float(gl_InstanceID) * 23.0; * * // Apply the offset to the final position. * gl_Position = uProjectionMatrix * uModelViewMatrix * (positionVec4 - * vec4(xOffset, 0.0, 0.0, 0.0)); * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = `#version 300 es * * precision mediump float; * * out vec4 outColor; * flat in int instanceID; * uniform float numInstances; * * void main() { * vec4 red = vec4(1.0, 0.0, 0.0, 1.0); * vec4 blue = vec4(0.0, 0.0, 1.0, 1.0); * * // Normalize the instance ID. * float normId = float(instanceID) / numInstances; * * // Mix between two colors using the normalized instance ID. * outColor = mix(red, blue, normId); * } * `; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * let myShader = createShader(vertSrc, fragSrc); * * background(220); * * // Compile and apply the p5.Shader. * shader(myShader); * * // Set the numInstances uniform. * myShader.setUniform('numInstances', 4); * * // Translate the origin to help align the drawing. * translate(25, -10); * * // Style the shapes. * noStroke(); * * // Draw the shapes. * beginShape(); * vertex(0, 0); * vertex(0, 20); * vertex(20, 20); * vertex(20, 0); * vertex(0, 0); * endShape(CLOSE, 4); * * describe('A row of four squares. Their colors transition from purple on the left to red on the right'); * } */ fn.endShape = function(mode, count = 1) { // p5._validateParameters('endShape', arguments); if (count < 1) { console.log('🌸 p5.js says: You can not have less than one instance'); count = 1; } this._renderer.endShape(mode, count); }; /** * Sets the normal vector for vertices in a custom 3D shape. * * 3D shapes created with beginShape() and * endShape() are made by connecting sets of * points called vertices. Each vertex added with * vertex() has a normal vector that points away * from it. The normal vector controls how light reflects off the shape. * * `normal()` can be called two ways with different parameters to define the * normal vector's components. * * The first way to call `normal()` has three parameters, `x`, `y`, and `z`. * If `Number`s are passed, as in `normal(1, 2, 3)`, they set the x-, y-, and * z-components of the normal vector. * * The second way to call `normal()` has one parameter, `vector`. If a * p5.Vector object is passed, as in * `normal(myVector)`, its components will be used to set the normal vector. * * `normal()` changes the normal vector of vertices added to a custom shape * with vertex(). `normal()` must be called between * the beginShape() and * endShape() functions, just like * vertex(). The normal vector set by calling * `normal()` will affect all following vertices until `normal()` is called * again: * * ```js * beginShape(); * * // Set the vertex normal. * normal(-0.4, -0.4, 0.8); * * // Add a vertex. * vertex(-30, -30, 0); * * // Set the vertex normal. * normal(0, 0, 1); * * // Add vertices. * vertex(30, -30, 0); * vertex(30, 30, 0); * * // Set the vertex normal. * normal(0.4, -0.4, 0.8); * * // Add a vertex. * vertex(-30, 30, 0); * * endShape(); * ``` * * @method normal * @param {p5.Vector} vector vertex normal as a p5.Vector object. * @chainable * * @example * // Click the and drag the mouse to view the scene from a different angle. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.' * ); * } * * function draw() { * background(0); * * // Enable orbiting with the mouse. * orbitControl(); * * // Style the shape. * normalMaterial(); * noStroke(); * * // Draw the shape. * beginShape(); * vertex(-30, -30, 0); * vertex(30, -30, 0); * vertex(30, 30, 0); * vertex(-30, 30, 0); * endShape(); * } * * @example * // Click the and drag the mouse to view the scene from a different angle. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.' * ); * } * * function draw() { * background(0); * * // Enable orbiting with the mouse. * orbitControl(); * * // Style the shape. * normalMaterial(); * noStroke(); * * // Draw the shape. * // Use normal() to set vertex normals. * beginShape(); * normal(-0.4, -0.4, 0.8); * vertex(-30, -30, 0); * * normal(0, 0, 1); * vertex(30, -30, 0); * vertex(30, 30, 0); * * normal(0.4, -0.4, 0.8); * vertex(-30, 30, 0); * endShape(); * } * * @example * // Click the and drag the mouse to view the scene from a different angle. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.' * ); * } * * function draw() { * background(0); * * // Enable orbiting with the mouse. * orbitControl(); * * // Style the shape. * normalMaterial(); * noStroke(); * * // Create p5.Vector objects. * let n1 = createVector(-0.4, -0.4, 0.8); * let n2 = createVector(0, 0, 1); * let n3 = createVector(0.4, -0.4, 0.8); * * // Draw the shape. * // Use normal() to set vertex normals. * beginShape(); * normal(n1); * vertex(-30, -30, 0); * * normal(n2); * vertex(30, -30, 0); * vertex(30, 30, 0); * * normal(n3); * vertex(-30, 30, 0); * endShape(); * } */ /** * @method normal * @param {Number} x x-component of the vertex normal. * @param {Number} y y-component of the vertex normal. * @param {Number} z z-component of the vertex normal. * @chainable */ fn.normal = function(x, y, z) { this._assert3d('normal'); // p5._validateParameters('normal', arguments); this._renderer.normal(...arguments); return this; }; /** * Sets the shader's vertex property or attribute variables. * * A vertex property, or vertex attribute, is a variable belonging to a vertex in a shader. p5.js provides some * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are * set using vertex(), normal() * and fill() respectively. Custom properties can also * be defined within beginShape() and * endShape(). * * The first parameter, `propertyName`, is a string with the property's name. * This is the same variable name which should be declared in the shader, such as * `in vec3 aProperty`, similar to .`setUniform()`. * * The second parameter, `data`, is the value assigned to the shader variable. This * value will be applied to subsequent vertices created with * vertex(). It can be a Number or an array of numbers, * and in the shader program the type can be declared according to the WebGL * specification. Common types include `float`, `vec2`, `vec3`, `vec4` or matrices. * * See also the vertexProperty() method on * Geometry objects. * * @method vertexProperty * @for p5 * @param {String} attributeName the name of the vertex attribute. * @param {Number|Number[]} data the data tied to the vertex attribute. * * @example * const vertSrc = `#version 300 es * precision mediump float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * in vec3 aPosition; * in vec2 aOffset; * * void main(){ * vec4 positionVec4 = vec4(aPosition.xyz, 1.0); * positionVec4.xy += aOffset; * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * const fragSrc = `#version 300 es * precision mediump float; * out vec4 outColor; * void main(){ * outColor = vec4(0.0, 1.0, 1.0, 1.0); * } * `; * * function setup(){ * createCanvas(100, 100, WEBGL); * * // Create and use the custom shader. * const myShader = createShader(vertSrc, fragSrc); * shader(myShader); * * describe('A wobbly, cyan circle on a gray background.'); * } * * function draw(){ * // Set the styles * background(125); * noStroke(); * * // Draw the circle. * beginShape(); * for (let i = 0; i < 30; i++){ * const x = 40 * cos(i/30 * TWO_PI); * const y = 40 * sin(i/30 * TWO_PI); * * // Apply some noise to the coordinates. * const xOff = 10 * noise(x + millis()/1000) - 5; * const yOff = 10 * noise(y + millis()/1000) - 5; * * // Apply these noise values to the following vertex. * vertexProperty('aOffset', [xOff, yOff]); * vertex(x, y); * } * endShape(CLOSE); * } * * @example * let myShader; * const cols = 10; * const rows = 10; * const cellSize = 6; * * const vertSrc = `#version 300 es * precision mediump float; * uniform mat4 uProjectionMatrix; * uniform mat4 uModelViewMatrix; * * in vec3 aPosition; * in vec3 aNormal; * in vec3 aVertexColor; * in float aDistance; * * out vec3 vVertexColor; * * void main(){ * vec4 positionVec4 = vec4(aPosition, 1.0); * positionVec4.xyz += aDistance * aNormal * 2.0;; * vVertexColor = aVertexColor; * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * const fragSrc = `#version 300 es * precision mediump float; * * in vec3 vVertexColor; * out vec4 outColor; * * void main(){ * outColor = vec4(vVertexColor, 1.0); * } * `; * * function setup(){ * createCanvas(100, 100, WEBGL); * * // Create and apply the custom shader. * myShader = createShader(vertSrc, fragSrc); * shader(myShader); * noStroke(); * describe('A blue grid, which moves away from the mouse position, on a gray background.'); * } * * function draw(){ * background(200); * * // Draw the grid in the middle of the screen. * translate(-cols*cellSize/2, -rows*cellSize/2); * beginShape(QUADS); * for (let i = 0; i < cols; i++) { * for (let j = 0; j < rows; j++) { * * // Calculate the cell position. * let x = i * cellSize; * let y = j * cellSize; * * fill(j/rows*255, j/cols*255, 255); * * // Calculate the distance from the corner of each cell to the mouse. * let distance = dist(x, y, mouseX, mouseY); * * // Send the distance to the shader. * vertexProperty('aDistance', min(distance, 100)); * * vertex(x, y); * vertex(x + cellSize, y); * vertex(x + cellSize, y + cellSize); * vertex(x, y + cellSize); * } * } * endShape(); * } */ fn.vertexProperty = function(attributeName, data){ // this._assert3d('vertexProperty'); // p5._validateParameters('vertexProperty', arguments); this._renderer.vertexProperty(attributeName, data); }; } if(typeof p5 !== 'undefined'){ vertex(p5, p5.prototype); } /** * @module Color * @submodule Setting * @for p5 * @requires core * @requires constants */ function setting(p5, fn){ /** * Starts defining a shape that will mask any shapes drawn afterward. * * Any shapes drawn between `beginClip()` and * endClip() will add to the mask shape. The mask * will apply to anything drawn after endClip(). * * The parameter, `options`, is optional. If an object with an `invert` * property is passed, as in `beginClip({ invert: true })`, it will be used to * set the masking mode. `{ invert: true }` inverts the mask, creating holes * in shapes that are masked. `invert` is `false` by default. * * Masks can be contained between the * push() and pop() functions. * Doing so allows unmasked shapes to be drawn after masked shapes. * * Masks can also be defined in a callback function that's passed to * clip(). * * @method beginClip * @param {Object} [options] an object containing clip settings. * @param {Boolean} [options.invert=false] Whether or not to invert the mask. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a mask. * beginClip(); * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * endClip(); * * // Draw a backing shape. * square(5, 5, 45); * * describe('A white triangle and circle on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create an inverted mask. * beginClip({ invert: true }); * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * endClip(); * * // Draw a backing shape. * square(5, 5, 45); * * describe('A white square at the top-left corner of a gray square. The white square has a triangle and a circle cut out of it.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * noStroke(); * * // Draw a masked shape. * push(); * // Create a mask. * beginClip(); * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * endClip(); * * // Draw a backing shape. * square(5, 5, 45); * pop(); * * // Translate the origin to the center. * translate(50, 50); * * // Draw an inverted masked shape. * push(); * // Create an inverted mask. * beginClip({ invert: true }); * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * endClip(); * * // Draw a backing shape. * square(5, 5, 45); * pop(); * * describe('In the top left, a white triangle and circle. In the bottom right, a white square with a triangle and circle cut out of it.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A silhouette of a rotating torus colored fuchsia.'); * } * * function draw() { * background(200); * * // Create a mask. * beginClip(); * push(); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * scale(0.5); * torus(30, 15); * pop(); * endClip(); * * // Draw a backing shape. * noStroke(); * fill('fuchsia'); * plane(100); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A silhouette of a rotating torus colored with a gradient from cyan to purple.'); * } * * function draw() { * background(200); * * // Create a mask. * beginClip(); * push(); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * scale(0.5); * torus(30, 15); * pop(); * endClip(); * * // Draw a backing shape. * noStroke(); * beginShape(QUAD_STRIP); * fill(0, 255, 255); * vertex(-width / 2, -height / 2); * vertex(width / 2, -height / 2); * fill(100, 0, 100); * vertex(-width / 2, height / 2); * vertex(width / 2, height / 2); * endShape(); * } */ fn.beginClip = function(options = {}) { this._renderer.beginClip(options); }; /** * Ends defining a mask that was started with * beginClip(). * * @method endClip * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a mask. * beginClip(); * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * endClip(); * * // Draw a backing shape. * square(5, 5, 45); * * describe('A white triangle and circle on a gray background.'); * } */ fn.endClip = function() { this._renderer.endClip(); }; /** * Defines a shape that will mask any shapes drawn afterward. * * The first parameter, `callback`, is a function that defines the mask. * Any shapes drawn in `callback` will add to the mask shape. The mask * will apply to anything drawn after `clip()` is called. * * The second parameter, `options`, is optional. If an object with an `invert` * property is passed, as in `beginClip({ invert: true })`, it will be used to * set the masking mode. `{ invert: true }` inverts the mask, creating holes * in shapes that are masked. `invert` is `false` by default. * * Masks can be contained between the * push() and pop() functions. * Doing so allows unmasked shapes to be drawn after masked shapes. * * Masks can also be defined with beginClip() * and endClip(). * * @method clip * @param {Function} callback a function that draws the mask shape. * @param {Object} [options] an object containing clip settings. * @param {Boolean} [options.invert=false] Whether or not to invert the mask. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a mask. * clip(mask); * * // Draw a backing shape. * square(5, 5, 45); * * describe('A white triangle and circle on a gray background.'); * } * * // Declare a function that defines the mask. * function mask() { * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create an inverted mask. * clip(mask, { invert: true }); * * // Draw a backing shape. * square(5, 5, 45); * * describe('A white square at the top-left corner of a gray square. The white square has a triangle and a circle cut out of it.'); * } * * // Declare a function that defines the mask. * function mask() { * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * noStroke(); * * // Draw a masked shape. * push(); * // Create a mask. * clip(mask); * * // Draw a backing shape. * square(5, 5, 45); * pop(); * * // Translate the origin to the center. * translate(50, 50); * * // Draw an inverted masked shape. * push(); * // Create an inverted mask. * clip(mask, { invert: true }); * * // Draw a backing shape. * square(5, 5, 45); * pop(); * * describe('In the top left, a white triangle and circle. In the bottom right, a white square with a triangle and circle cut out of it.'); * } * * // Declare a function that defines the mask. * function mask() { * triangle(15, 37, 30, 13, 43, 37); * circle(45, 45, 7); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A silhouette of a rotating torus colored fuchsia.'); * } * * function draw() { * background(200); * * // Create a mask. * clip(mask); * * // Draw a backing shape. * noStroke(); * fill('fuchsia'); * plane(100); * } * * // Declare a function that defines the mask. * function mask() { * push(); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * scale(0.5); * torus(30, 15); * pop(); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A silhouette of a rotating torus colored with a gradient from cyan to purple.'); * } * * function draw() { * background(200); * * // Create a mask. * clip(mask); * * // Draw a backing shape. * noStroke(); * beginShape(QUAD_STRIP); * fill(0, 255, 255); * vertex(-width / 2, -height / 2); * vertex(width / 2, -height / 2); * fill(100, 0, 100); * vertex(-width / 2, height / 2); * vertex(width / 2, height / 2); * endShape(); * } * * // Declare a function that defines the mask. * function mask() { * push(); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * scale(0.5); * torus(30, 15); * pop(); * } */ fn.clip = function(callback, options) { this._renderer.beginClip(options); callback(); this._renderer.endClip(options); }; /** * Sets the color used for the background of the canvas. * * By default, the background is transparent. `background()` is typically used * within draw() to clear the display window at the * beginning of each frame. It can also be used inside * setup() to set the background on the first frame * of animation. * * The version of `background()` with one parameter interprets the value one * of four ways. If the parameter is a `Number`, it's interpreted as a grayscale * value. If the parameter is a `String`, it's interpreted as a CSS color string. * RGB, RGBA, HSL, HSLA, hex, and named color strings are supported. If the * parameter is a p5.Color object, it will be used as * the background color. If the parameter is a * p5.Image object, it will be used as the background * image. * * The version of `background()` with two parameters interprets the first one * as a grayscale value. The second parameter sets the alpha (transparency) * value. * * The version of `background()` with three parameters interprets them as RGB, * HSB, or HSL colors, depending on the current * colorMode(). By default, colors are specified * in RGB values. Calling `background(255, 204, 0)` sets the background a bright * yellow color. * * The version of `background()` with four parameters interprets them as RGBA, * HSBA, or HSLA colors, depending on the current * colorMode(). The last parameter sets the alpha * (transparency) value. * * @method background * @param {p5.Color} color any value created by the color() function * @chainable * * @example * function setup() { * createCanvas(100, 100); * * // A grayscale value. * background(51); * * describe('A canvas with a dark charcoal gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // A grayscale value and an alpha value. * background(51, 0.4); * describe('A canvas with a transparent gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // R, G & B values. * background(255, 204, 0); * * describe('A canvas with a yellow background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // R, G, B, and Alpha values. * background(255, 0, 0, 128); * * describe('A canvas with a semi-transparent red background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Use HSB color. * colorMode(HSB); * * // H, S & B values. * background(255, 204, 100); * * describe('A canvas with a royal blue background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // A CSS named color. * background('red'); * * describe('A canvas with a red background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Three-digit hex RGB notation. * background('#fae'); * * describe('A canvas with a pink background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Six-digit hex RGB notation. * background('#222222'); * * describe('A canvas with a black background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Integer RGB notation. * background('rgb(0, 255, 0)'); * * describe('A canvas with a bright green background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Integer RGBA notation. * background('rgba(0, 255, 0, 0.25)'); * * describe('A canvas with a transparent green background.'); * } * < * @example * function setup() { * createCanvas(100, 100); * * // Percentage RGB notation. * background('rgb(100%, 0%, 10%)'); * * describe('A canvas with a red background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Percentage RGBA notation. * background('rgba(100%, 0%, 100%, 0.5)'); * * describe('A canvas with a transparent purple background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // A p5.Color object. * let c = color(0, 0, 255); * background(c); * * describe('A canvas with a blue background.'); * } */ /** * @method background * @param {String} colorstring color string, possible formats include: integer * rgb() or rgba(), percentage rgb() or rgba(), * 3-digit hex, 6-digit hex. * @param {Number} [a] opacity of the background relative to current * color range (default is 0-255). * @chainable */ /** * @method background * @param {Number} gray specifies a value between white and black. * @param {Number} [a] * @chainable */ /** * @method background * @param {Number} v1 red value if color mode is RGB, or hue value if color mode is HSB. * @param {Number} v2 green value if color mode is RGB, or saturation value if color mode is HSB. * @param {Number} v3 blue value if color mode is RGB, or brightness value if color mode is HSB. * @param {Number} [a] * @chainable */ /** * @method background * @param {Number[]} values an array containing the red, green, blue * and alpha components of the color. * @chainable */ /** * @method background * @param {p5.Image} image image created with loadImage() * or createImage(), * to set as background. * (must be same size as the sketch window). * @param {Number} [a] * @chainable */ fn.background = function(...args) { this._renderer.background(...args); return this; }; /** * Clears the pixels on the canvas. * * `clear()` makes every pixel 100% transparent. Calling `clear()` doesn't * clear objects created by `createX()` functions such as * createGraphics(), * createVideo(), and * createImg(). These objects will remain * unchanged after calling `clear()` and can be redrawn. * * In WebGL mode, this function can clear the screen to a specific color. It * interprets four numeric parameters as normalized RGBA color values. It also * clears the depth buffer. If you are not using the WebGL renderer, these * parameters will have no effect. * * @method clear * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * describe('A gray square. White circles are drawn as the user moves the mouse. The circles disappear when the user presses the mouse.'); * } * * function draw() { * circle(mouseX, mouseY, 20); * } * * function mousePressed() { * clear(); * background(200); * } * * @example * let pg; * * function setup() { * createCanvas(100, 100); * background(200); * * pg = createGraphics(60, 60); * pg.background(200); * pg.noStroke(); * pg.circle(pg.width / 2, pg.height / 2, 15); * image(pg, 20, 20); * * describe('A white circle drawn on a gray square. The square gets smaller when the mouse is pressed.'); * } * * function mousePressed() { * clear(); * image(pg, 20, 20); * } * * @param {Number} [r] normalized red value. * @param {Number} [g] normalized green value. * @param {Number} [b] normalized blue value. * @param {Number} [a] normalized alpha value. */ fn.clear = function(...args) { const _r = args[0] || 0; const _g = args[1] || 0; const _b = args[2] || 0; const _a = args[3] || 0; this._renderer.clear(_r, _g, _b, _a); return this; }; /** * Changes the way color values are interpreted. * * By default, the `Number` parameters for fill(), * stroke(), * background(), and * color() are defined by values between 0 and 255 * using the RGB color model. This is equivalent to calling * `colorMode(RGB, 255)`. Pure red is `color(255, 0, 0)` in this model. * * Calling `colorMode(RGB, 100)` sets colors to use RGB color values * between 0 and 100. Pure red is `color(100, 0, 0)` in this model. * * Calling `colorMode(HSB)` or `colorMode(HSL)` changes to HSB or HSL systems instead of RGB. * Pure red is `color(0, 100, 100)` in HSB and `color(0, 100, 50)` in HSL. * * Some additional color modes that p5.js supports are: * * `RGBHDR` - High Dynamic Range RGB defined within the Display P3 color space. * Colors are expressed with an extended dynamic range. To render these colors * accurately, you must use the HDR canvas. * * `HWB` - Hue, Whiteness, Blackness. * Similar to HSB and HSL, this mode uses a hue angle. * Instead of saturation and lightness, HWB defines colors based on the percentage * of whiteness and blackness. This is the color model used by Chrome's GUI color picker. * Pure red in HWB is represented as `color(0, 0, 0)` (i.e., hue 0 with 0% whiteness and 0% blackness). * * * * `LAB` - Also known as CIE Lab, this color mode defines colors with Lightness, Alpha, and Beta. * It is widely used in professional color measurement contexts due to its perceptual uniformity. * * `LCH` - A more intuitive representation of the CIE Lab color space using Lightness, Chroma, and Hue. * This mode separates the color's chromatic intensity (chroma) from its lightness, * simplifying color selection and manipulation. * * `OKLAB` - A variant of the CIE Lab color space that corrects for non-uniformities inherent in LAB. * The adjustment provides a more perceptually accurate and uniform representation, * which is particularly beneficial for smooth color transitions. * * `OKLCH` - An easier-to-use representation of OKLAB, expressing colors in terms of Lightness, Chroma, and Hue. * This mode retains the perceptual benefits of OKLAB while offering a more intuitive format for color manipulation. * * p5.Color objects remember the mode that they were * created in. Changing modes doesn't affect their appearance. * * `Single-value (Grayscale) Colors`: * When a color is specified with only one parameter (e.g., `color(g)`), p5.js will interpret it * as a grayscale color. However, how that single parameter translates into a grayscale value * depends on the color mode: * * - `RGB, HSB, and HSL`: In RGB, the single value is interpreted using the “blue” maximum * (i.e., the single parameter is mapped to the blue channel's max). * In HSB and HSL, the single value is mapped to Brightness and Lightness max respectively with hue=0 . * and saturation=0. * * - `LAB, LCH, OKLAB, and OKLCH`: The single value is taken to be the `lightness (L)` component, * with the specified max range for that channel. * * - `HWB`: Grayscale relies on both the `whiteness (W)` and `blackness (B)` channels. Since * a single value cannot directly account for two distinct channels, the library uses an * average of their max values to interpret the single grayscale parameter. For instance, * if W has a max of 50 and B has a max of 100, then the single grayscale parameter * is mapped using (50 + 100) / 2 = 75 as its effective maximum. More complex or negative * ranges are currently not handled, so results in those cases may be ambiguous. * * @method colorMode * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode either RGB, HSB, HSL, * or one of the extended modes described above. * @param {Number} [max] range for all values. * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Fill with pure red. * fill(255, 0, 0); * * circle(50, 50, 25); * * describe('A gray square with a red circle at its center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use RGB color with values in the range 0-100. * colorMode(RGB, 100); * * // Fill with pure red. * fill(100, 0, 0); * * circle(50, 50, 25); * * describe('A gray square with a red circle at its center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSB color. * colorMode(HSB); * * // Fill with pure red. * fill(0, 100, 100); * * circle(50, 50, 25); * * describe('A gray square with a red circle at its center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSL color. * colorMode(HSL); * * // Fill with pure red. * fill(0, 100, 50); * * circle(50, 50, 25); * * describe('A gray square with a red circle at its center.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Draw a neutral gray background using the default color mode. * background(200); * * // Switch to HWB color mode. * // (Assuming p5.js supports HWB with a range of: * // hue: 0–360, whiteness: 0–100, blackness: 0–100.) * colorMode(HWB); * * // Set fill to pure red in HWB. * // Pure red in HWB is: hue = 0°, whiteness = 0%, blackness = 0%. * fill(0, 0, 0); * * // Draw a circle at the center. * circle(50, 50, 25); * * describe('A gray square with a red circle at its center, drawn using HWB color mode.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Draw a neutral gray background using the default color mode. * background(200); * * // Switch to LAB color mode. * // In this mode, L typically ranges from 0 to 100 while a and b span roughly -128 to 127. * colorMode(LAB); * * // Set fill to pure red in LAB. * // The sRGB red (255, 0, 0) converts approximately to LAB as: * // L = 53, a = 80, b = 67. * fill(53, 80, 67); * * // Draw a circle at the center. * circle(50, 50, 25); * * describe('A gray square with a red circle at its center, drawn using LAB color mode.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Draw a neutral gray background. * background(200); * * // Switch to LCH color mode. * // In LCH, colors are defined by Lightness, Chroma, and Hue (in degrees). * colorMode(LCH); * * // Set fill to an approximation of pure red in LCH: * // Lightness ≈ 53, Chroma ≈ 104, Hue ≈ 40°. * fill(53, 104, 40); * * // Draw a circle at the center. * circle(50, 50, 25); * * describe('A gray square with a red circle at its center, drawn using LCH color mode.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Use RGB color with values in the range 0-100. * colorMode(RGB, 100); * * for (let x = 0; x < 100; x += 1) { * for (let y = 0; y < 100; y += 1) { * stroke(x, y, 0); * point(x, y); * } * } * * describe( * 'A diagonal green to red gradient from bottom-left to top-right with shading transitioning to black at top-left corner.' * ); * } * * @example * function setup() { * createCanvas(100, 100); * * // Use HSB color with values in the range 0-100. * colorMode(HSB, 100); * * for (let x = 0; x < 100; x += 1) { * for (let y = 0; y < 100; y += 1) { * stroke(x, y, 100); * point(x, y); * } * } * * describe('A rainbow gradient from left-to-right. Brightness transitions to white at the top.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Create a p5.Color object. * let myColor = color(180, 175, 230); * background(myColor); * * // Use RGB color with values in the range 0-1. * colorMode(RGB, 1); * * // Get the red, green, and blue color components. * let redValue = red(myColor); * let greenValue = green(myColor); * let blueValue = blue(myColor); * * // Round the color components for display. * redValue = round(redValue, 2); * greenValue = round(greenValue, 2); * blueValue = round(blueValue, 2); * * // Display the color components. * text(`Red: ${redValue}`, 10, 10, 80, 80); * text(`Green: ${greenValue}`, 10, 40, 80, 80); * text(`Blue: ${blueValue}`, 10, 70, 80, 80); * * describe('A purple canvas with the red, green, and blue decimal values of the color written on it.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(255); * * // Use RGB color with alpha values in the range 0-1. * colorMode(RGB, 255, 255, 255, 1); * * noFill(); * strokeWeight(4); * stroke(255, 0, 10, 0.3); * circle(40, 40, 50); * circle(50, 60, 50); * * describe('Two overlapping translucent pink circle outlines.'); * } * * @example * let hslGraphic, lchGraphic, oklchGraphic; * * function setup() { * createCanvas(600, 200); * noLoop(); * * // Create three graphics objects for HSL, LCH, and OKLCH color modes * hslGraphic = createGraphics(200, 200); * lchGraphic = createGraphics(200, 200); * oklchGraphic = createGraphics(200, 200); * * // Draw HSL color wheel * colorMode(HSL); * hslGraphic.translate(100, 100); * for (let i = 0; i < 1000; i++) { * hslGraphic.stroke(360 / 1000 * i, 70, 50); * hslGraphic.line(0, 0, hslGraphic.width / 2, 0); * hslGraphic.rotate(TAU / 1000); * } * * // Draw LCH color wheel * colorMode(LCH); * lchGraphic.translate(100, 100); * for (let i = 0; i < 1000; i++) { * lchGraphic.stroke(54, 106, 360 / 1000 * i); * lchGraphic.line(0, 0, lchGraphic.width / 2, 0); * lchGraphic.rotate(TAU / 1000); * } * * // Draw OKLCH color wheel * colorMode(OKLCH); * oklchGraphic.translate(100, 100); * for (let i = 0; i < 1000; i++) { * oklchGraphic.stroke(54, 106, 360 / 1000 * i); * oklchGraphic.line(0, 0, oklchGraphic.width / 2, 0); * oklchGraphic.rotate(TAU / 1000); * } * } * * function draw() { * // Set the styles * colorMode(RGB); * background(220); * * // Display the color wheels * image(hslGraphic, 0, 0); * image(lchGraphic, 200, 0); * image(oklchGraphic, 400, 0); * } * * @example * // Example: Single-value (Grayscale) colors in different color modes. * // The rectangle is filled with one parameter, but its final color depends * // on how that parameter is interpreted by the current color mode. * * function setup() { * createCanvas(100, 100); * noStroke(); * noLoop(); * } * * function draw() { * // Set color mode to RGB with range 0-255 * colorMode(RGB, 255); * * // Fill with single grayscale value * fill(128); * rect(0, 0, 100, 100); * * // Add text label * fill(0); // Switch to black text for clarity * textSize(14); * text("RGB (128)", 10, 20); * } */ /** * @method colorMode * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode * @param {Number} max1 range for the red or hue depending on the * current color mode. * @param {Number} max2 range for the green or saturation depending * on the current color mode. * @param {Number} max3 range for the blue or brightness/lightness * depending on the current color mode. * @param {Number} [maxA] range for the alpha. * * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode. */ /** * @method colorMode * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode. */ fn.colorMode = function(mode, max1, max2, max3, maxA) { // p5._validateParameters('colorMode', arguments); if ( [ RGB, RGBHDR, HSB, HSL, HWB, LAB, LCH, OKLAB, OKLCH ].includes(mode) ) { // Set color mode. this._renderer.states.setValue('colorMode', mode); // Set color maxes. this._renderer.states.setValue('colorMaxes', this._renderer.states.colorMaxes.clone()); const maxes = this._renderer.states.colorMaxes[mode]; if (arguments.length === 2) { maxes[0] = max1; // Red maxes[1] = max1; // Green maxes[2] = max1; // Blue maxes[3] = max1; // Alpha } else if (arguments.length === 4) { maxes[0] = max1; // Red maxes[1] = max2; // Green maxes[2] = max3; // Blue } else if (arguments.length === 5) { maxes[0] = max1; // Red maxes[1] = max2; // Green maxes[2] = max3; // Blue maxes[3] = maxA; // Alpha } } return this._renderer.states.colorMode; }; /** * Sets the color used to fill shapes. * * Calling `fill(255, 165, 0)` or `fill('orange')` means all shapes drawn * after the fill command will be filled with the color orange. * * The version of `fill()` with one parameter interprets the value one of * three ways. If the parameter is a `Number`, it's interpreted as a grayscale * value. If the parameter is a `String`, it's interpreted as a CSS color * string. A p5.Color object can also be provided to * set the fill color. * * The version of `fill()` with three parameters interprets them as RGB, HSB, * or HSL colors, depending on the current * colorMode(). The default color space is RGB, * with each value in the range from 0 to 255. * * The version of `fill()` with four parameters interprets them as `RGBA`, `HSBA`, * or `HSLA` colors, depending on the current colorMode(). The last parameter * sets the alpha (transparency) value. * * @method fill * @param {Number} v1 red value if color mode is RGB or hue value if color mode is HSB. * @param {Number} v2 green value if color mode is RGB or saturation value if color mode is HSB. * @param {Number} v3 blue value if color mode is RGB or brightness value if color mode is HSB. * @param {Number} [alpha] alpha value, controls transparency (0 - transparent, 255 - opaque). * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // A grayscale value. * fill(51); * square(20, 20, 60); * * describe('A dark charcoal gray square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // R, G & B values. * fill(255, 204, 0); * square(20, 20, 60); * * describe('A yellow square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // R, G, B, and Alpha values. * fill(255, 0, 0, 128); * square(20, 20, 60); * * describe('A semi-transparent red square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(100); * * // Use HSB color. * colorMode(HSB); * * // H, S & B values. * fill(255, 204, 100); * square(20, 20, 60); * * describe('A royal blue square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // A CSS named color. * fill('red'); * square(20, 20, 60); * * describe('A red square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Three-digit hex RGB notation. * fill('#fae'); * square(20, 20, 60); * * describe('A pink square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Six-digit hex RGB notation. * fill('#A251FA'); * square(20, 20, 60); * * describe('A purple square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Integer RGB notation. * fill('rgb(0, 255, 0)'); * square(20, 20, 60); * * describe('A bright green square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Integer RGBA notation. * fill('rgba(0, 255, 0, 0.25)'); * square(20, 20, 60); * * describe('A soft green rectange with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Percentage RGB notation. * fill('rgb(100%, 0%, 10%)'); * square(20, 20, 60); * * describe('A red square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Percentage RGBA notation. * fill('rgba(100%, 0%, 100%, 0.5)'); * square(20, 20, 60); * * describe('A dark fuchsia square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // A p5.Color object. * let c = color(0, 0, 255); * fill(c); * square(20, 20, 60); * * describe('A blue square with a black outline.'); * } */ /** * @method fill * @param {String} value a color string. * @chainable */ /** * @method fill * @param {Number} gray a grayscale value. * @param {Number} [alpha] * @chainable */ /** * @method fill * @param {Number[]} values an array containing the red, green, blue & * and alpha components of the color. * @chainable */ /** * @method fill * @param {p5.Color} color the fill color. * @chainable */ fn.fill = function(...args) { this._renderer.fill(...args); return this; }; /** * Disables setting the fill color for shapes. * * Calling `noFill()` is the same as making the fill completely transparent, * as in `fill(0, 0)`. If both noStroke() and * `noFill()` are called, nothing will be drawn to the screen. * * @method noFill * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the top square. * square(32, 10, 35); * * // Draw the bottom square. * noFill(); * square(32, 55, 35); * * describe('A white square on above an empty square. Both squares have black outlines.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A purple cube wireframe spinning on a black canvas.'); * } * * function draw() { * background(0); * * // Style the box. * noFill(); * stroke(100, 100, 240); * * // Rotate the coordinates. * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * * // Draw the box. * box(45); * } */ fn.noFill = function() { this._renderer.noFill(); return this; }; /** * Disables drawing points, lines, and the outlines of shapes. * * Calling `noStroke()` is the same as making the stroke completely transparent, * as in `stroke(0, 0)`. If both `noStroke()` and * noFill() are called, nothing will be drawn to the * screen. * * @method noStroke * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(200); * * noStroke(); * square(20, 20, 60); * * describe('A white square with no outline.'); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A pink cube with no edge outlines spinning on a black canvas.'); * } * * function draw() { * background(0); * * // Style the box. * noStroke(); * fill(240, 150, 150); * * // Rotate the coordinates. * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * * // Draw the box. * box(45); * } */ fn.noStroke = function() { this._renderer.states.setValue('strokeColor', null); return this; }; /** * Sets the color used to draw points, lines, and the outlines of shapes. * * Calling `stroke(255, 165, 0)` or `stroke('orange')` means all shapes drawn * after calling `stroke()` will be outlined with the color orange. The way * these parameters are interpreted may be changed with the * colorMode() function. * * The version of `stroke()` with one parameter interprets the value one of * three ways. If the parameter is a `Number`, it's interpreted as a grayscale * value. If the parameter is a `String`, it's interpreted as a CSS color * string. A p5.Color object can also be provided to * set the stroke color. * * The version of `stroke()` with two parameters interprets the first one as a * grayscale value. The second parameter sets the alpha (transparency) value. * * The version of `stroke()` with three parameters interprets them as RGB, HSB, * or HSL colors, depending on the current `colorMode()`. * * The version of `stroke()` with four parameters interprets them as RGBA, HSBA, * or HSLA colors, depending on the current `colorMode()`. The last parameter * sets the alpha (transparency) value. * * @method stroke * @param {Number} v1 red value if color mode is RGB or hue value if color mode is HSB. * @param {Number} v2 green value if color mode is RGB or saturation value if color mode is HSB. * @param {Number} v3 blue value if color mode is RGB or brightness value if color mode is HSB. * @param {Number} [alpha] alpha value, controls transparency (0 - transparent, 255 - opaque). * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // A grayscale value. * strokeWeight(4); * stroke(51); * square(20, 20, 60); * * describe('A white square with a dark charcoal gray outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // R, G & B values. * stroke(255, 204, 0); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a yellow outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use HSB color. * colorMode(HSB); * * // H, S & B values. * strokeWeight(4); * stroke(255, 204, 100); * square(20, 20, 60); * * describe('A white square with a royal blue outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // A CSS named color. * stroke('red'); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a red outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Three-digit hex RGB notation. * stroke('#fae'); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a pink outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Six-digit hex RGB notation. * stroke('#222222'); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Integer RGB notation. * stroke('rgb(0, 255, 0)'); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a bright green outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Integer RGBA notation. * stroke('rgba(0, 255, 0, 0.25)'); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a soft green outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Percentage RGB notation. * stroke('rgb(100%, 0%, 10%)'); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a red outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Percentage RGBA notation. * stroke('rgba(100%, 0%, 100%, 0.5)'); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a dark fuchsia outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // A p5.Color object. * stroke(color(0, 0, 255)); * strokeWeight(4); * square(20, 20, 60); * * describe('A white square with a blue outline.'); * } */ /** * @method stroke * @param {String} value a color string. * @chainable */ /** * @method stroke * @param {Number} gray a grayscale value. * @param {Number} [alpha] * @chainable */ /** * @method stroke * @param {Number[]} values an array containing the red, green, blue, * and alpha components of the color. * @chainable */ /** * @method stroke * @param {p5.Color} color the stroke color. * @chainable */ fn.stroke = function(...args) { this._renderer.stroke(...args); return this; }; /** * Starts using shapes to erase parts of the canvas. * * All drawing that follows `erase()` will subtract from the canvas, revealing * the web page underneath. The erased areas will become transparent, allowing * the content behind the canvas to show through. The * fill(), stroke(), and * blendMode() have no effect once `erase()` is * called. * * The `erase()` function has two optional parameters. The first parameter * sets the strength of erasing by the shape's interior. A value of 0 means * that no erasing will occur. A value of 255 means that the shape's interior * will fully erase the content underneath. The default value is 255 * (full strength). * * The second parameter sets the strength of erasing by the shape's edge. A * value of 0 means that no erasing will occur. A value of 255 means that the * shape's edge will fully erase the content underneath. The default value is * 255 (full strength). * * To cancel the erasing effect, use the noErase() * function. * * `erase()` has no effect on drawing done with the * image() and * background() functions. * * @method erase * @param {Number} [strengthFill] a number (0-255) for the strength of erasing under a shape's interior. * Defaults to 255, which is full strength. * @param {Number} [strengthStroke] a number (0-255) for the strength of erasing under a shape's edge. * Defaults to 255, which is full strength. * * @chainable * * @example * function setup() { * createCanvas(100, 100); * * background(100, 100, 250); * * // Draw a pink square. * fill(250, 100, 100); * square(20, 20, 60); * * // Erase a circular area. * erase(); * circle(25, 30, 30); * noErase(); * * describe('A purple canvas with a pink square in the middle. A circle is erased from the top-left, leaving a hole.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(100, 100, 250); * * // Draw a pink square. * fill(250, 100, 100); * square(20, 20, 60); * * // Erase a circular area. * strokeWeight(5); * erase(150, 255); * circle(25, 30, 30); * noErase(); * * describe('A purple canvas with a pink square in the middle. A circle at the top-left partially erases its interior and a fully erases its outline.'); * } */ fn.erase = function(opacityFill = 255, opacityStroke = 255) { this._renderer.erase(opacityFill, opacityStroke); return this; }; /** * Ends erasing that was started with erase(). * * The fill(), stroke(), and * blendMode() settings will return to what they * were prior to calling erase(). * * @method noErase * @chainable * @example * function setup() { * createCanvas(100, 100); * * background(235, 145, 15); * * // Draw the left rectangle. * noStroke(); * fill(30, 45, 220); * rect(30, 10, 10, 80); * * // Erase a circle. * erase(); * circle(50, 50, 60); * noErase(); * * // Draw the right rectangle. * rect(70, 10, 10, 80); * * describe('An orange canvas with two tall blue rectangles. A circular hole in the center erases the rectangle on the left but not the one on the right.'); * } */ fn.noErase = function() { this._renderer.noErase(); return this; }; /** * Sets the way colors blend when added to the canvas. * * By default, drawing with a solid color paints over the current pixel values * on the canvas. `blendMode()` offers many options for blending colors. * * Shapes, images, and text can be used as sources for drawing to the canvas. * A source pixel changes the color of the canvas pixel where it's drawn. The * final color results from blending the source pixel's color with the canvas * pixel's color. RGB color values from the source and canvas pixels are * compared, added, subtracted, multiplied, and divided to create different * effects. Red values with red values, greens with greens, and blues with * blues. * * The parameter, `mode`, sets the blend mode. For example, calling * `blendMode(ADD)` sets the blend mode to `ADD`. The following blend modes * are available in both 2D and WebGL mode: * * - `BLEND`: color values from the source overwrite the canvas. This is the default mode. * - `ADD`: color values from the source are added to values from the canvas. * - `DARKEST`: keeps the darkest color value. * - `LIGHTEST`: keeps the lightest color value. * - `EXCLUSION`: similar to `DIFFERENCE` but with less contrast. * - `MULTIPLY`: color values from the source are multiplied with values from the canvas. The result is always darker. * - `SCREEN`: all color values are inverted, then multiplied, then inverted again. The result is always lighter. (Opposite of `MULTIPLY`) * - `REPLACE`: the last source drawn completely replaces the rest of the canvas. * - `REMOVE`: overlapping pixels are removed by making them completely transparent. * * The following blend modes are only available in 2D mode: * * - `DIFFERENCE`: color values from the source are subtracted from the values from the canvas. If the difference is a negative number, it's made positive. * - `OVERLAY`: combines `MULTIPLY` and `SCREEN`. Dark values in the canvas get darker and light values get lighter. * - `HARD_LIGHT`: combines `MULTIPLY` and `SCREEN`. Dark values in the source get darker and light values get lighter. * - `SOFT_LIGHT`: a softer version of `HARD_LIGHT`. * - `DODGE`: lightens light tones and increases contrast. Divides the canvas color values by the inverted color values from the source. * - `BURN`: darkens dark tones and increases contrast. Divides the source color values by the inverted color values from the canvas, then inverts the result. * * The following blend modes are only available in WebGL mode: * * - `SUBTRACT`: RGB values from the source are subtracted from the values from the canvas. If the difference is a negative number, it's made positive. Alpha (transparency) values from the source and canvas are added. * * @method blendMode * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT)} mode blend mode to set. * either BLEND, DARKEST, LIGHTEST, DIFFERENCE, MULTIPLY, * EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT, * SOFT_LIGHT, DODGE, BURN, ADD, REMOVE or SUBTRACT * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use the default blend mode. * blendMode(BLEND); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A blue line and a red line form an X on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(ADD); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(DARKEST); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(LIGHTEST); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(EXCLUSION); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A yellow line and a cyan line form an X on a gray background. The area where they overlap is green.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(MULTIPLY); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(SCREEN); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(REPLACE); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A diagonal red line.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(REMOVE); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('The silhouette of an X is missing from a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(DIFFERENCE); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A yellow line and a cyan line form an X on a gray background. The area where they overlap is green.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(OVERLAY); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is bright magenta.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(HARD_LIGHT); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A blue line and a red line form an X on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(SOFT_LIGHT); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is violet.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(DODGE); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint violet.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(BURN); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set the blend mode. * blendMode(SUBTRACT); * * // Style the lines. * strokeWeight(30); * * // Draw the blue line. * stroke('blue'); * line(25, 25, 75, 75); * * // Draw the red line. * stroke('red'); * line(75, 25, 25, 75); * * describe('A yellow line and a turquoise line form an X on a gray background. The area where they overlap is green.'); * } */ fn.blendMode = function (mode) { // p5._validateParameters('blendMode', arguments); if (mode === NORMAL) { // Warning added 3/26/19, can be deleted in future (1.0 release?) console.warn( 'NORMAL has been deprecated for use in blendMode. defaulting to BLEND instead.' ); mode = BLEND; } this._renderer.blendMode(mode); }; } if(typeof p5 !== 'undefined'){ setting(p5, p5.prototype); } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var GifReader_1; var GifWriter_1; function GifWriter(buf, width, height, gopts) { var p = 0; var gopts = gopts === undefined ? { } : gopts; var loop_count = gopts.loop === undefined ? null : gopts.loop; var global_palette = gopts.palette === undefined ? null : gopts.palette; if (width <= 0 || height <= 0 || width > 65535 || height > 65535) throw new Error("Width/Height invalid."); function check_palette_and_num_colors(palette) { var num_colors = palette.length; if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) { throw new Error( "Invalid code/color length, must be power of 2 and 2 .. 256."); } return num_colors; } // - Header. buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a // Handling of Global Color Table (palette) and background index. var gp_num_colors_pow2 = 0; var background = 0; if (global_palette !== null) { var gp_num_colors = check_palette_and_num_colors(global_palette); while (gp_num_colors >>= 1) ++gp_num_colors_pow2; gp_num_colors = 1 << gp_num_colors_pow2; --gp_num_colors_pow2; if (gopts.background !== undefined) { background = gopts.background; if (background >= gp_num_colors) throw new Error("Background index out of range."); // The GIF spec states that a background index of 0 should be ignored, so // this is probably a mistake and you really want to set it to another // slot in the palette. But actually in the end most browsers, etc end // up ignoring this almost completely (including for dispose background). if (background === 0) throw new Error("Background index explicitly passed as 0."); } } // - Logical Screen Descriptor. // NOTE(deanm): w/h apparently ignored by implementations, but set anyway. buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff; buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff; // NOTE: Indicates 0-bpp original color resolution (unused?). buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag. gp_num_colors_pow2; // NOTE: No sort flag (unused?). buf[p++] = background; // Background Color Index. buf[p++] = 0; // Pixel aspect ratio (unused?). // - Global Color Table if (global_palette !== null) { for (var i = 0, il = global_palette.length; i < il; ++i) { var rgb = global_palette[i]; buf[p++] = rgb >> 16 & 0xff; buf[p++] = rgb >> 8 & 0xff; buf[p++] = rgb & 0xff; } } if (loop_count !== null) { // Netscape block for looping. if (loop_count < 0 || loop_count > 65535) throw new Error("Loop count invalid.") // Extension code, label, and length. buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b; // NETSCAPE2.0 buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53; buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45; buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30; // Sub-block buf[p++] = 0x03; buf[p++] = 0x01; buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff; buf[p++] = 0x00; // Terminator. } var ended = false; this.addFrame = function(x, y, w, h, indexed_pixels, opts) { if (ended === true) { --p; ended = false; } // Un-end. opts = opts === undefined ? { } : opts; // TODO(deanm): Bounds check x, y. Do they need to be within the virtual // canvas width/height, I imagine? if (x < 0 || y < 0 || x > 65535 || y > 65535) throw new Error("x/y invalid.") if (w <= 0 || h <= 0 || w > 65535 || h > 65535) throw new Error("Width/Height invalid.") if (indexed_pixels.length < w * h) throw new Error("Not enough pixels for the frame size."); var using_local_palette = true; var palette = opts.palette; if (palette === undefined || palette === null) { using_local_palette = false; palette = global_palette; } if (palette === undefined || palette === null) throw new Error("Must supply either a local or global palette."); var num_colors = check_palette_and_num_colors(palette); // Compute the min_code_size (power of 2), destroying num_colors. var min_code_size = 0; while (num_colors >>= 1) ++min_code_size; num_colors = 1 << min_code_size; // Now we can easily get it back. var delay = opts.delay === undefined ? 0 : opts.delay; // From the spec: // 0 - No disposal specified. The decoder is // not required to take any action. // 1 - Do not dispose. The graphic is to be left // in place. // 2 - Restore to background color. The area used by the // graphic must be restored to the background color. // 3 - Restore to previous. The decoder is required to // restore the area overwritten by the graphic with // what was there prior to rendering the graphic. // 4-7 - To be defined. // NOTE(deanm): Dispose background doesn't really work, apparently most // browsers ignore the background palette index and clear to transparency. var disposal = opts.disposal === undefined ? 0 : opts.disposal; if (disposal < 0 || disposal > 3) // 4-7 is reserved. throw new Error("Disposal out of range."); var use_transparency = false; var transparent_index = 0; if (opts.transparent !== undefined && opts.transparent !== null) { use_transparency = true; transparent_index = opts.transparent; if (transparent_index < 0 || transparent_index >= num_colors) throw new Error("Transparent color index."); } if (disposal !== 0 || use_transparency || delay !== 0) { // - Graphics Control Extension buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label. buf[p++] = 4; // Byte size. buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0); buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff; buf[p++] = transparent_index; // Transparent color index. buf[p++] = 0; // Block Terminator. } // - Image Descriptor buf[p++] = 0x2c; // Image Seperator. buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left. buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top. buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff; buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff; // NOTE: No sort flag (unused?). // TODO(deanm): Support interlace. buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0; // - Local Color Table if (using_local_palette === true) { for (var i = 0, il = palette.length; i < il; ++i) { var rgb = palette[i]; buf[p++] = rgb >> 16 & 0xff; buf[p++] = rgb >> 8 & 0xff; buf[p++] = rgb & 0xff; } } p = GifWriterOutputLZWCodeStream( buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels); return p; }; this.end = function() { if (ended === false) { buf[p++] = 0x3b; // Trailer. ended = true; } return p; }; this.getOutputBuffer = function() { return buf; }; this.setOutputBuffer = function(v) { buf = v; }; this.getOutputBufferPosition = function() { return p; }; this.setOutputBufferPosition = function(v) { p = v; }; } // Main compression routine, palette indexes -> LZW code stream. // |index_stream| must have at least one entry. function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) { buf[p++] = min_code_size; var cur_subblock = p++; // Pointing at the length field. var clear_code = 1 << min_code_size; var code_mask = clear_code - 1; var eoi_code = clear_code + 1; var next_code = eoi_code + 1; var cur_code_size = min_code_size + 1; // Number of bits per code. var cur_shift = 0; // We have at most 12-bit codes, so we should have to hold a max of 19 // bits here (and then we would write out). var cur = 0; function emit_bytes_to_buffer(bit_block_size) { while (cur_shift >= bit_block_size) { buf[p++] = cur & 0xff; cur >>= 8; cur_shift -= 8; if (p === cur_subblock + 256) { // Finished a subblock. buf[cur_subblock] = 255; cur_subblock = p++; } } } function emit_code(c) { cur |= c << cur_shift; cur_shift += cur_code_size; emit_bytes_to_buffer(8); } // I am not an expert on the topic, and I don't want to write a thesis. // However, it is good to outline here the basic algorithm and the few data // structures and optimizations here that make this implementation fast. // The basic idea behind LZW is to build a table of previously seen runs // addressed by a short id (herein called output code). All data is // referenced by a code, which represents one or more values from the // original input stream. All input bytes can be referenced as the same // value as an output code. So if you didn't want any compression, you // could more or less just output the original bytes as codes (there are // some details to this, but it is the idea). In order to achieve // compression, values greater then the input range (codes can be up to // 12-bit while input only 8-bit) represent a sequence of previously seen // inputs. The decompressor is able to build the same mapping while // decoding, so there is always a shared common knowledge between the // encoding and decoder, which is also important for "timing" aspects like // how to handle variable bit width code encoding. // // One obvious but very important consequence of the table system is there // is always a unique id (at most 12-bits) to map the runs. 'A' might be // 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship // can be used for an effecient lookup strategy for the code mapping. We // need to know if a run has been seen before, and be able to map that run // to the output code. Since we start with known unique ids (input bytes), // and then from those build more unique ids (table entries), we can // continue this chain (almost like a linked list) to always have small // integer values that represent the current byte chains in the encoder. // This means instead of tracking the input bytes (AAAABCD) to know our // current state, we can track the table entry for AAAABC (it is guaranteed // to exist by the nature of the algorithm) and the next character D. // Therefor the tuple of (table_entry, byte) is guaranteed to also be // unique. This allows us to create a simple lookup key for mapping input // sequences to codes (table indices) without having to store or search // any of the code sequences. So if 'AAAA' has a table entry of 12, the // tuple of ('AAAA', K) for any input byte K will be unique, and can be our // key. This leads to a integer value at most 20-bits, which can always // fit in an SMI value and be used as a fast sparse array / object key. // Output code for the current contents of the index buffer. var ib_code = index_stream[0] & code_mask; // Load first input index. var code_table = { }; // Key'd on our 20-bit "tuple". emit_code(clear_code); // Spec says first code should be a clear code. // First index already loaded, process the rest of the stream. for (var i = 1, il = index_stream.length; i < il; ++i) { var k = index_stream[i] & code_mask; var cur_key = ib_code << 8 | k; // (prev, k) unique tuple. var cur_code = code_table[cur_key]; // buffer + k. // Check if we have to create a new code table entry. if (cur_code === undefined) { // We don't have buffer + k. // Emit index buffer (without k). // This is an inline version of emit_code, because this is the core // writing routine of the compressor (and V8 cannot inline emit_code // because it is a closure here in a different context). Additionally // we can call emit_byte_to_buffer less often, because we can have // 30-bits (from our 31-bit signed SMI), and we know our codes will only // be 12-bits, so can safely have 18-bits there without overflow. // emit_code(ib_code); cur |= ib_code << cur_shift; cur_shift += cur_code_size; while (cur_shift >= 8) { buf[p++] = cur & 0xff; cur >>= 8; cur_shift -= 8; if (p === cur_subblock + 256) { // Finished a subblock. buf[cur_subblock] = 255; cur_subblock = p++; } } if (next_code === 4096) { // Table full, need a clear. emit_code(clear_code); next_code = eoi_code + 1; cur_code_size = min_code_size + 1; code_table = { }; } else { // Table not full, insert a new entry. // Increase our variable bit code sizes if necessary. This is a bit // tricky as it is based on "timing" between the encoding and // decoder. From the encoders perspective this should happen after // we've already emitted the index buffer and are about to create the // first table entry that would overflow our current code bit size. if (next_code >= (1 << cur_code_size)) ++cur_code_size; code_table[cur_key] = next_code++; // Insert into code table. } ib_code = k; // Index buffer to single input k. } else { ib_code = cur_code; // Index buffer to sequence in code table. } } emit_code(ib_code); // There will still be something in the index buffer. emit_code(eoi_code); // End Of Information. // Flush / finalize the sub-blocks stream to the buffer. emit_bytes_to_buffer(1); // Finish the sub-blocks, writing out any unfinished lengths and // terminating with a sub-block of length 0. If we have already started // but not yet used a sub-block it can just become the terminator. if (cur_subblock + 1 === p) { // Started but unused. buf[cur_subblock] = 0; } else { // Started and used, write length and additional terminator block. buf[cur_subblock] = p - cur_subblock - 1; buf[p++] = 0; } return p; } function GifReader(buf) { var p = 0; // - Header (GIF87a or GIF89a). if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 || buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) { throw new Error("Invalid GIF 87a/89a header."); } // - Logical Screen Descriptor. var width = buf[p++] | buf[p++] << 8; var height = buf[p++] | buf[p++] << 8; var pf0 = buf[p++]; // . var global_palette_flag = pf0 >> 7; var num_global_colors_pow2 = pf0 & 0x7; var num_global_colors = 1 << (num_global_colors_pow2 + 1); buf[p++]; buf[p++]; // Pixel aspect ratio (unused?). var global_palette_offset = null; var global_palette_size = null; if (global_palette_flag) { global_palette_offset = p; global_palette_size = num_global_colors; p += num_global_colors * 3; // Seek past palette. } var no_eof = true; var frames = [ ]; var delay = 0; var transparent_index = null; var disposal = 0; // 0 - No disposal specified. var loop_count = null; this.width = width; this.height = height; while (no_eof && p < buf.length) { switch (buf[p++]) { case 0x21: // Graphics Control Extension Block switch (buf[p++]) { case 0xff: // Application specific block // Try if it's a Netscape block (with animation loop counter). if (buf[p ] !== 0x0b || // 21 FF already read, check block size. // NETSCAPE2.0 buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 && buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 && buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 && buf[p+10] == 0x2e && buf[p+11] == 0x30 && // Sub-block buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) { p += 14; loop_count = buf[p++] | buf[p++] << 8; p++; // Skip terminator. } else { // We don't know what it is, just try to get past it. p += 12; while (true) { // Seek through subblocks. var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator p += block_size; } } break; case 0xf9: // Graphics Control Extension if (buf[p++] !== 0x4 || buf[p+4] !== 0) throw new Error("Invalid graphics extension block."); var pf1 = buf[p++]; delay = buf[p++] | buf[p++] << 8; transparent_index = buf[p++]; if ((pf1 & 1) === 0) transparent_index = null; disposal = pf1 >> 2 & 0x7; p++; // Skip terminator. break; case 0xfe: // Comment Extension. while (true) { // Seek through subblocks. var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator // console.log(buf.slice(p, p+block_size).toString('ascii')); p += block_size; } break; default: throw new Error( "Unknown graphic control label: 0x" + buf[p-1].toString(16)); } break; case 0x2c: // Image Descriptor. var x = buf[p++] | buf[p++] << 8; var y = buf[p++] | buf[p++] << 8; var w = buf[p++] | buf[p++] << 8; var h = buf[p++] | buf[p++] << 8; var pf2 = buf[p++]; var local_palette_flag = pf2 >> 7; var interlace_flag = pf2 >> 6 & 1; var num_local_colors_pow2 = pf2 & 0x7; var num_local_colors = 1 << (num_local_colors_pow2 + 1); var palette_offset = global_palette_offset; var palette_size = global_palette_size; var has_local_palette = false; if (local_palette_flag) { var has_local_palette = true; palette_offset = p; // Override with local palette. palette_size = num_local_colors; p += num_local_colors * 3; // Seek past palette. } var data_offset = p; p++; // codesize while (true) { var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator p += block_size; } frames.push({x: x, y: y, width: w, height: h, has_local_palette: has_local_palette, palette_offset: palette_offset, palette_size: palette_size, data_offset: data_offset, data_length: p - data_offset, transparent_index: transparent_index, interlaced: !!interlace_flag, delay: delay, disposal: disposal}); break; case 0x3b: // Trailer Marker (end of file). no_eof = false; break; default: throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16)); } } this.numFrames = function() { return frames.length; }; this.loopCount = function() { return loop_count; }; this.frameInfo = function(frame_num) { if (frame_num < 0 || frame_num >= frames.length) throw new Error("Frame index out of range."); return frames[frame_num]; }; this.decodeAndBlitFrameBGRA = function(frame_num, pixels) { var frame = this.frameInfo(frame_num); var num_pixels = frame.width * frame.height; var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( buf, frame.data_offset, index_stream, num_pixels); var palette_offset = frame.palette_offset; // NOTE(deanm): It seems to be much faster to compare index to 256 than // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in // the profile, not sure if it's related to using a Uint8Array. var trans = frame.transparent_index; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subrect within the framerect, so the additional pixels // must be skipped over after we finished a scanline. var framewidth = frame.width; var framestride = width - framewidth; var xleft = framewidth; // Number of subrect pixels left in scanline. // Output indicies of the top left and bottom right corners of the subrect. var opbeg = ((frame.y * width) + frame.x) * 4; var opend = ((frame.y + frame.height) * width + frame.x) * 4; var op = opbeg; var scanstride = framestride * 4; // Use scanstride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanstride += width * 4 * 7; // Pass 1. } var interlaceskip = 8; // Tracking the row interval in the current pass. for (var i = 0, il = index_stream.length; i < il; ++i) { var index = index_stream[i]; if (xleft === 0) { // Beginning of new scan line op += scanstride; xleft = framewidth; if (op >= opend) { // Catch the wrap to switch passes when interlacing. scanstride = framestride * 4 + width * 4 * (interlaceskip-1); // interlaceskip / 2 * 4 is interlaceskip << 1. op = opbeg + (framewidth + framestride) * (interlaceskip << 1); interlaceskip >>= 1; } } if (index === trans) { op += 4; } else { var r = buf[palette_offset + index * 3]; var g = buf[palette_offset + index * 3 + 1]; var b = buf[palette_offset + index * 3 + 2]; pixels[op++] = b; pixels[op++] = g; pixels[op++] = r; pixels[op++] = 255; } --xleft; } }; // I will go to copy and paste hell one day... this.decodeAndBlitFrameRGBA = function(frame_num, pixels) { var frame = this.frameInfo(frame_num); var num_pixels = frame.width * frame.height; var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( buf, frame.data_offset, index_stream, num_pixels); var palette_offset = frame.palette_offset; // NOTE(deanm): It seems to be much faster to compare index to 256 than // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in // the profile, not sure if it's related to using a Uint8Array. var trans = frame.transparent_index; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subrect within the framerect, so the additional pixels // must be skipped over after we finished a scanline. var framewidth = frame.width; var framestride = width - framewidth; var xleft = framewidth; // Number of subrect pixels left in scanline. // Output indicies of the top left and bottom right corners of the subrect. var opbeg = ((frame.y * width) + frame.x) * 4; var opend = ((frame.y + frame.height) * width + frame.x) * 4; var op = opbeg; var scanstride = framestride * 4; // Use scanstride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanstride += width * 4 * 7; // Pass 1. } var interlaceskip = 8; // Tracking the row interval in the current pass. for (var i = 0, il = index_stream.length; i < il; ++i) { var index = index_stream[i]; if (xleft === 0) { // Beginning of new scan line op += scanstride; xleft = framewidth; if (op >= opend) { // Catch the wrap to switch passes when interlacing. scanstride = framestride * 4 + width * 4 * (interlaceskip-1); // interlaceskip / 2 * 4 is interlaceskip << 1. op = opbeg + (framewidth + framestride) * (interlaceskip << 1); interlaceskip >>= 1; } } if (index === trans) { op += 4; } else { var r = buf[palette_offset + index * 3]; var g = buf[palette_offset + index * 3 + 1]; var b = buf[palette_offset + index * 3 + 2]; pixels[op++] = r; pixels[op++] = g; pixels[op++] = b; pixels[op++] = 255; } --xleft; } }; } function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) { var min_code_size = code_stream[p++]; var clear_code = 1 << min_code_size; var eoi_code = clear_code + 1; var next_code = eoi_code + 1; var cur_code_size = min_code_size + 1; // Number of bits per code. // NOTE: This shares the same name as the encoder, but has a different // meaning here. Here this masks each code coming from the code stream. var code_mask = (1 << cur_code_size) - 1; var cur_shift = 0; var cur = 0; var op = 0; // Output pointer. var subblock_size = code_stream[p++]; // TODO(deanm): Would using a TypedArray be any faster? At least it would // solve the fast mode / backing store uncertainty. // var code_table = Array(4096); var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits. var prev_code = null; // Track code-1. while (true) { // Read up to two bytes, making sure we always 12-bits for max sized code. while (cur_shift < 16) { if (subblock_size === 0) break; // No more data to be read. cur |= code_stream[p++] << cur_shift; cur_shift += 8; if (subblock_size === 1) { // Never let it get to 0 to hold logic above. subblock_size = code_stream[p++]; // Next subblock. } else { --subblock_size; } } // TODO(deanm): We should never really get here, we should have received // and EOI. if (cur_shift < cur_code_size) break; var code = cur & code_mask; cur >>= cur_code_size; cur_shift -= cur_code_size; // TODO(deanm): Maybe should check that the first code was a clear code, // at least this is what you're supposed to do. But actually our encoder // now doesn't emit a clear code first anyway. if (code === clear_code) { // We don't actually have to clear the table. This could be a good idea // for greater error checking, but we don't really do any anyway. We // will just track it with next_code and overwrite old entries. next_code = eoi_code + 1; cur_code_size = min_code_size + 1; code_mask = (1 << cur_code_size) - 1; // Don't update prev_code ? prev_code = null; continue; } else if (code === eoi_code) { break; } // We have a similar situation as the decoder, where we want to store // variable length entries (code table entries), but we want to do in a // faster manner than an array of arrays. The code below stores sort of a // linked list within the code table, and then "chases" through it to // construct the dictionary entries. When a new entry is created, just the // last byte is stored, and the rest (prefix) of the entry is only // referenced by its table entry. Then the code chases through the // prefixes until it reaches a single byte code. We have to chase twice, // first to compute the length, and then to actually copy the data to the // output (backwards, since we know the length). The alternative would be // storing something in an intermediate stack, but that doesn't make any // more sense. I implemented an approach where it also stored the length // in the code table, although it's a bit tricky because you run out of // bits (12 + 12 + 8), but I didn't measure much improvements (the table // entries are generally not the long). Even when I created benchmarks for // very long table entries the complexity did not seem worth it. // The code table stores the prefix entry in 12 bits and then the suffix // byte in 8 bits, so each entry is 20 bits. var chase_code = code < next_code ? code : prev_code; // Chase what we will output, either {CODE} or {CODE-1}. var chase_length = 0; var chase = chase_code; while (chase > clear_code) { chase = code_table[chase] >> 8; ++chase_length; } var k = chase; var op_end = op + chase_length + (chase_code !== code ? 1 : 0); if (op_end > output_length) { console.log("Warning, gif stream longer than expected."); return; } // Already have the first byte from the chase, might as well write it fast. output[op++] = k; op += chase_length; var b = op; // Track pointer, writing backwards. if (chase_code !== code) // The case of emitting {CODE-1} + k. output[op++] = k; chase = chase_code; while (chase_length--) { chase = code_table[chase]; output[--b] = chase & 0xff; // Write backwards. chase >>= 8; // Pull down to the prefix code. } if (prev_code !== null && next_code < 4096) { code_table[next_code++] = prev_code << 8 | k; // TODO(deanm): Figure out this clearing vs code growth logic better. I // have an feeling that it should just happen somewhere else, for now it // is awkward between when we grow past the max and then hit a clear code. // For now just check if we hit the max 12-bits (then a clear code should // follow, also of course encoded in 12-bits). if (next_code >= code_mask+1 && cur_code_size < 12) { ++cur_code_size; code_mask = code_mask << 1 | 1; } } prev_code = code; } if (op !== output_length) { console.log("Warning, gif stream shorter than expected."); } return output; } // CommonJS. try { GifWriter_1 = GifWriter; GifReader_1 = GifReader; } catch(e) {} /** * @module Image * @submodule Image * @for p5 * @requires core */ function image$1(p5, fn){ /** * Creates a new p5.Image object. * * `createImage()` uses the `width` and `height` parameters to set the new * p5.Image object's dimensions in pixels. The new * p5.Image can be modified by updating its * pixels array or by calling its * get() and * set() methods. The * loadPixels() method must be called * before reading or modifying pixel values. The * updatePixels() method must be called * for updates to take effect. * * Note: The new p5.Image object is transparent by * default. * * @method createImage * @param {Integer} width width in pixels. * @param {Integer} height height in pixels. * @return {p5.Image} new p5.Image object. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels into memory. * img.loadPixels(); * * // Set all the image's pixels to black. * for (let x = 0; x < img.width; x += 1) { * for (let y = 0; y < img.height; y += 1) { * img.set(x, y, 0); * } * } * * // Update the image's pixel values. * img.updatePixels(); * * // Draw the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the image's pixels into memory. * img.loadPixels(); * * // Create a color gradient. * for (let x = 0; x < img.width; x += 1) { * for (let y = 0; y < img.height; y += 1) { * // Calculate the transparency. * let a = map(x, 0, img.width, 0, 255); * * // Create a p5.Color object. * let c = color(0, a); * * // Set the pixel's color. * img.set(x, y, c); * } * } * * // Update the image's pixels. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A square with a horizontal color gradient that transitions from gray to black.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Image object. * let img = createImage(66, 66); * * // Load the pixels into memory. * img.loadPixels(); * // Get the current pixel density. * let d = pixelDensity(); * * // Calculate the pixel that is halfway through the image's pixel array. * let halfImage = 4 * (d * img.width) * (d * img.height / 2); * * // Set half of the image's pixels to black. * for (let i = 0; i < halfImage; i += 4) { * // Red. * img.pixels[i] = 0; * // Green. * img.pixels[i + 1] = 0; * // Blue. * img.pixels[i + 2] = 0; * // Alpha. * img.pixels[i + 3] = 255; * } * * // Update the image's pixels. * img.updatePixels(); * * // Display the image. * image(img, 17, 17); * * describe('A black square drawn in the middle of a gray square.'); * } */ fn.createImage = function(width, height) { // p5._validateParameters('createImage', arguments); return new p5.Image(width, height); }; /** * Saves the current canvas as an image. * * By default, `saveCanvas()` saves the canvas as a PNG image called * `untitled.png`. * * The first parameter, `filename`, is optional. It's a string that sets the * file's name. If a file extension is included, as in * `saveCanvas('drawing.png')`, then the image will be saved using that * format. * * The second parameter, `extension`, is also optional. It sets the files format. * Either `'png'`, `'webp'`, or `'jpg'` can be used. For example, `saveCanvas('drawing', 'jpg')` * saves the canvas to a file called `drawing.jpg`. * * Note: The browser will either save the file immediately or prompt the user * with a dialogue window. * * @method saveCanvas * @param {p5.Framebuffer|p5.Element|HTMLCanvasElement} selectedCanvas reference to a * specific HTML5 canvas element. * @param {String} [filename] file name. Defaults to 'untitled'. * @param {String} [extension] file extension, either 'png', 'webp', or 'jpg'. Defaults to 'png'. * * @example * // META:norender * function setup() { * createCanvas(100, 100); * background(255); * * // Save the canvas to 'untitled.png'. * saveCanvas(); * * describe('A white square.'); * } * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(255); * * // Save the canvas to 'myCanvas.jpg'. * saveCanvas('myCanvas.jpg'); * * describe('A white square.'); * } * * @example * // META:norender * function setup() { * createCanvas(100, 100); * * background(255); * * // Save the canvas to 'myCanvas.jpg'. * saveCanvas('myCanvas', 'jpg'); * * describe('A white square.'); * } * * @example * // META:norender * function setup() { * let cnv = createCanvas(100, 100); * * background(255); * * // Save the canvas to 'untitled.png'. * saveCanvas(cnv); * * describe('A white square.'); * } * * @example * // META:norender * function setup() { * let cnv = createCanvas(100, 100); * * background(255); * * // Save the canvas to 'myCanvas.jpg'. * saveCanvas(cnv, 'myCanvas.jpg'); * * describe('A white square.'); * } * * @example * // META:norender * function setup() { * let cnv = createCanvas(100, 100); * * background(255); * * // Save the canvas to 'myCanvas.jpg'. * saveCanvas(cnv, 'myCanvas', 'jpg'); * * describe('A white square.'); * } */ /** * @method saveCanvas * @param {String} [filename] * @param {String} [extension] */ fn.saveCanvas = function(...args) { // copy arguments to array let htmlCanvas, filename, extension, temporaryGraphics; if (args[0] instanceof HTMLCanvasElement) { htmlCanvas = args[0]; args.shift(); } else if (args[0] instanceof Element) { htmlCanvas = args[0].elt; args.shift(); } else if (args[0] instanceof Framebuffer$1) { const framebuffer = args[0]; temporaryGraphics = this.createGraphics(framebuffer.width, framebuffer.height); temporaryGraphics.pixelDensity(framebuffer.pixelDensity()); framebuffer.loadPixels(); temporaryGraphics.loadPixels(); temporaryGraphics.pixels.set(framebuffer.pixels); temporaryGraphics.updatePixels(); htmlCanvas = temporaryGraphics._renderer.canvas; args.shift(); } else { htmlCanvas = this._curElement && this._curElement.elt; } if (args.length >= 1) { filename = args[0]; } if (args.length >= 2) { extension = args[1]; } extension = extension || fn._checkFileExtension(filename, extension)[1] || 'png'; let mimeType; switch (extension) { default: //case 'png': mimeType = 'image/png'; break; case 'webp': mimeType = 'image/webp'; break; case 'jpeg': case 'jpg': mimeType = 'image/jpeg'; break; } htmlCanvas.toBlob(blob => { fn.downloadFile(blob, filename, extension); if(temporaryGraphics) temporaryGraphics.remove(); }, mimeType); }; // 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. fn.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 GifWriter_1( 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(); allFramesPixelColors[i].forEach((color, 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' }); fn.downloadFile(blob, filename, extension); }; /** * Captures a sequence of frames from the canvas that can be saved as images. * * `saveFrames()` creates an array of frame objects. Each frame is stored as * an object with its file type, file name, and image data as a string. For * example, the first saved frame might have the following properties: * * `{ ext: 'png', filenmame: 'frame0', imageData: 'data:image/octet-stream;base64, abc123' }`. * * The first parameter, `filename`, sets the prefix for the file names. For * example, setting the prefix to `'frame'` would generate the image files * `frame0.png`, `frame1.png`, and so on. * * The second parameter, `extension`, sets the file type to either `'png'` or * `'jpg'`. * * The third parameter, `duration`, sets the duration to record in seconds. * The maximum duration is 15 seconds. * * The fourth parameter, `framerate`, sets the number of frames to record per * second. The maximum frame rate value is 22. Limits are placed on `duration` * and `framerate` to avoid using too much memory. Recording large canvases * can easily crash sketches or even web browsers. * * The fifth parameter, `callback`, is optional. If a function is passed, * image files won't be saved by default. The callback function can be used * to process an array containing the data for each captured frame. The array * of image data contains a sequence of objects with three properties for each * frame: `imageData`, `filename`, and `extension`. * * Note: Frames are downloaded as individual image files by default. * * @method saveFrames * @param {String} filename prefix of file name. * @param {String} extension file extension, either 'jpg' or 'png'. * @param {Number} duration duration in seconds to record. This parameter will be constrained to be less or equal to 15. * @param {Number} framerate number of frames to save per second. This parameter will be constrained to be less or equal to 22. * @param {function(Array)} [callback] callback function that will be executed * to handle the image data. This function * should accept an array as argument. The * array will contain the specified number of * frames of objects. Each object has three * properties: `imageData`, `filename`, and `extension`. * @example * function setup() { * createCanvas(100, 100); * * describe('A square repeatedly changes color from blue to pink.'); * } * * function draw() { * let r = frameCount % 255; * let g = 50; * let b = 100; * background(r, g, b); * } * * // Save the frames when the user presses the 's' key. * function keyPressed() { * if (key === 's') { * saveFrames('frame', 'png', 1, 5); * } * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A square repeatedly changes color from blue to pink.'); * } * * function draw() { * let r = frameCount % 255; * let g = 50; * let b = 100; * background(r, g, b); * } * * // Print 5 frames when the user presses the mouse. * function mousePressed() { * saveFrames('frame', 'png', 1, 5, printFrames); * } * * // Prints an array of objects containing raw image data, filenames, and extensions. * function printFrames(frames) { * for (let frame of frames) { * print(frame); * } * } */ fn.saveFrames = function(fName, ext, _duration, _fps, callback) { // p5._validateParameters('saveFrames', arguments); let duration = _duration || 3; duration = Math.max(Math.min(duration, 15), 0); duration = duration * 1000; let fps = _fps || 15; fps = Math.max(Math.min(fps, 22), 0); let count = 0; const makeFrame = fn._makeFrame; const cnv = this._curElement.elt; let frames = []; const frameFactory = setInterval(() => { frames.push(makeFrame(fName + count, ext, cnv)); count++; }, 1000 / fps); setTimeout(() => { clearInterval(frameFactory); if (callback) { callback(frames); } else { for (const f of frames) { fn.downloadFile(f.imageData, f.filename, f.ext); } } frames = []; // clear frames }, duration + 0.01); }; fn._makeFrame = function(filename, extension, _cnv) { let cnv; if (this) { cnv = this._curElement.elt; } else { cnv = _cnv; } let mimeType; if (!extension) { extension = 'png'; mimeType = 'image/png'; } else { switch (extension.toLowerCase()) { case 'png': mimeType = 'image/png'; break; case 'jpeg': mimeType = 'image/jpeg'; break; case 'jpg': mimeType = 'image/jpeg'; break; default: mimeType = 'image/png'; break; } } const downloadMime = 'image/octet-stream'; let imageData = cnv.toDataURL(mimeType); imageData = imageData.replace(mimeType, downloadMime); const thisFrame = {}; thisFrame.imageData = imageData; thisFrame.filename = filename; thisFrame.ext = extension; return thisFrame; }; } if(typeof p5 !== 'undefined'){ image$1(p5, p5.prototype); } /* The MIT License (MIT) Copyright (c) 2019 Evan Plaice Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ function parse$3 (csv, options, reviver = v => v) { const ctx = Object.create(null); ctx.options = options || {}; ctx.reviver = reviver; ctx.value = ''; ctx.entry = []; ctx.output = []; ctx.col = 1; ctx.row = 1; ctx.options.delimiter = ctx.options.delimiter === undefined ? '"' : options.delimiter; if(ctx.options.delimiter.length > 1 || ctx.options.delimiter.length === 0) throw Error(`CSVError: delimiter must be one character [${ctx.options.separator}]`); ctx.options.separator = ctx.options.separator === undefined ? ',' : options.separator; if(ctx.options.separator.length > 1 || ctx.options.separator.length === 0) throw Error(`CSVError: separator must be one character [${ctx.options.separator}]`); const lexer = new RegExp(`${escapeRegExp(ctx.options.delimiter)}|${escapeRegExp(ctx.options.separator)}|\r\n|\n|\r|[^${escapeRegExp(ctx.options.delimiter)}${escapeRegExp(ctx.options.separator)}\r\n]+`, 'y'); const isNewline = /^(\r\n|\n|\r)$/; let matches = []; let match = ''; let state = 0; while ((matches = lexer.exec(csv)) !== null) { match = matches[0]; switch (state) { case 0: // start of entry switch (true) { case match === ctx.options.delimiter: state = 3; break; case match === ctx.options.separator: state = 0; valueEnd(ctx); break; case isNewline.test(match): state = 0; valueEnd(ctx); entryEnd(ctx); break; default: ctx.value += match; state = 2; break; } break; case 2: // un-delimited input switch (true) { case match === ctx.options.separator: state = 0; valueEnd(ctx); break; case isNewline.test(match): state = 0; valueEnd(ctx); entryEnd(ctx); break; default: state = 4; throw Error(`CSVError: Illegal state [row:${ctx.row}, col:${ctx.col}]`); } break; case 3: // delimited input switch (true) { case match === ctx.options.delimiter: state = 4; break; default: state = 3; ctx.value += match; break; } break; case 4: // escaped or closing delimiter switch (true) { case match === ctx.options.delimiter: state = 3; ctx.value += match; break; case match === ctx.options.separator: state = 0; valueEnd(ctx); break; case isNewline.test(match): state = 0; valueEnd(ctx); entryEnd(ctx); break; default: throw Error(`CSVError: Illegal state [row:${ctx.row}, col:${ctx.col}]`); } break; } } // flush the last value if (ctx.entry.length !== 0) { valueEnd(ctx); entryEnd(ctx); } return ctx.output; } function stringify (array, options = {}, replacer = v => v) { const ctx = Object.create(null); ctx.options = options; ctx.options.eof = ctx.options.eof !== undefined ? ctx.options.eof : true; ctx.row = 1; ctx.col = 1; ctx.output = ''; ctx.options.delimiter = ctx.options.delimiter === undefined ? '"' : options.delimiter; if(ctx.options.delimiter.length > 1 || ctx.options.delimiter.length === 0) throw Error(`CSVError: delimiter must be one character [${ctx.options.separator}]`); ctx.options.separator = ctx.options.separator === undefined ? ',' : options.separator; if(ctx.options.separator.length > 1 || ctx.options.separator.length === 0) throw Error(`CSVError: separator must be one character [${ctx.options.separator}]`); const needsDelimiters = new RegExp(`${escapeRegExp(ctx.options.delimiter)}|${escapeRegExp(ctx.options.separator)}|\r\n|\n|\r`); array.forEach((row, rIdx) => { let entry = ''; ctx.col = 1; row.forEach((col, cIdx) => { if (typeof col === 'string') { col = col.replace(new RegExp(ctx.options.delimiter, 'g'), `${ctx.options.delimiter}${ctx.options.delimiter}`); col = needsDelimiters.test(col) ? `${ctx.options.delimiter}${col}${ctx.options.delimiter}` : col; } entry += replacer(col, ctx.row, ctx.col); if (cIdx !== row.length - 1) { entry += ctx.options.separator; } ctx.col++; }); switch (true) { case ctx.options.eof: case !ctx.options.eof && rIdx !== array.length - 1: ctx.output += `${entry}\n`; break; default: ctx.output += `${entry}`; break; } ctx.row++; }); return ctx.output; } function valueEnd (ctx) { const value = ctx.options.typed ? inferType(ctx.value) : ctx.value; ctx.entry.push(ctx.reviver(value, ctx.row, ctx.col)); ctx.value = ''; ctx.col++; } function entryEnd (ctx) { ctx.output.push(ctx.entry); ctx.entry = []; ctx.row++; ctx.col = 1; } function inferType (value) { const isNumber = /.\./; switch (true) { case value === 'true': case value === 'false': return value === 'true'; case isNumber.test(value): return parseFloat(value); case isFinite(value): return parseInt(value); default: return value; } } function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } /** * @module IO * @submodule Input * @for p5 * @requires core */ class HTTPError extends Error { status; response; ok; } async function request(path, type){ try { const res = await fetch(path); if (res.ok) { let data; switch(type) { case 'json': data = await res.json(); break; case 'text': data = await res.text(); break; case 'arrayBuffer': data = await res.arrayBuffer(); break; case 'blob': data = await res.blob(); break; case 'bytes': // TODO: Chrome does not implement res.bytes() yet if(res.bytes){ data = await res.bytes(); }else { const d = await res.arrayBuffer(); data = new Uint8Array(d); } break; default: throw new Error('Unsupported response type'); } return { data, headers: res.headers }; } else { const err = new HTTPError(res.statusText); err.status = res.status; err.response = res; err.ok = false; throw err; } } catch(err) { // Handle both fetch error and HTTP error if (err instanceof TypeError) { console.log('You may have encountered a CORS error'); } else if (err instanceof HTTPError) { console.log('You have encountered a HTTP error'); } else if (err instanceof SyntaxError) { console.log('There is an error parsing the response to requested data structure'); } throw err; } } function files(p5, fn){ /** * Loads a JSON file to create an `Object`. * * JavaScript Object Notation * (JSON) * is a standard format for sending data between applications. The format is * based on JavaScript objects which have keys and values. JSON files store * data in an object with strings as keys. Values can be strings, numbers, * Booleans, arrays, `null`, or other objects. * * The first parameter, `path`, is a string with the path to the file. * Paths to local files should be relative, as in * `loadJSON('assets/data.json')`. URLs such as * `'https://example.com/data.json'` may be blocked due to browser security. * The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) * object for more advanced usage. * * The second parameter, `successCallback`, is optional. If a function is * passed, as in `loadJSON('assets/data.json', handleData)`, then the * `handleData()` function will be called once the data loads. The object * created from the JSON data will be passed to `handleData()` as its only argument. * The return value of the `handleData()` function will be used as the final return * value of `loadJSON('assets/data.json', handleData)`. * * The third parameter, `failureCallback`, is also optional. If a function is * passed, as in `loadJSON('assets/data.json', handleData, handleFailure)`, * then the `handleFailure()` function will be called if an error occurs while * loading. The `Error` object will be passed to `handleFailure()` as its only * argument. The return value of the `handleFailure()` function will be used as the * final return value of `loadJSON('assets/data.json', handleData, handleFailure)`. * * This function returns a `Promise` and should be used in an `async` setup with * `await`. See the examples for the usage syntax. * * @method loadJSON * @param {String|Request} path path of the JSON file to be loaded. * @param {Function} [successCallback] function to call once the data is loaded. Will be passed the object. * @param {Function} [errorCallback] function to call if the data fails to load. Will be passed an `Error` event object. * @return {Promise} object containing the loaded data. * * @example * let myData; * * async function setup() { * myData = await loadJSON('assets/data.json'); * createCanvas(100, 100); * * background(200); * * // Style the circle. * fill(myData.color); * noStroke(); * * // Draw the circle. * circle(myData.x, myData.y, myData.d); * * describe('A pink circle on a gray background.'); * } * * @example * let myData; * * async function setup() { * myData = await loadJSON('assets/data.json'); * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object and make it transparent. * let c = color(myData.color); * c.setAlpha(80); * * // Style the circles. * fill(c); * noStroke(); * * // Iterate over the myData.bubbles array. * for (let b of myData.bubbles) { * // Draw a circle for each bubble. * circle(b.x, b.y, b.d); * } * * describe('Several pink bubbles floating in a blue sky.'); * } * * @example * let myData; * * async function setup() { * myData = await loadJSON('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson'); * createCanvas(100, 100); * * background(200); * * // Get data about the most recent earthquake. * let quake = myData.features[0].properties; * * // Draw a circle based on the earthquake's magnitude. * circle(50, 50, quake.mag * 10); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(11); * * // Display the earthquake's location. * text(quake.place, 5, 80, 100); * * describe(`A white circle on a gray background. The text "${quake.place}" is written beneath the circle.`); * } * * @example * let bigQuake; * * // Load the GeoJSON and preprocess it. * async function setup() { * await loadJSON( * 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson', * handleData * ); * * createCanvas(100, 100); * * background(200); * * // Draw a circle based on the earthquake's magnitude. * circle(50, 50, bigQuake.mag * 10); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(11); * * // Display the earthquake's location. * text(bigQuake.place, 5, 80, 100); * * describe(`A white circle on a gray background. The text "${bigQuake.place}" is written beneath the circle.`); * } * * // Find the biggest recent earthquake. * function handleData(data) { * let maxMag = 0; * // Iterate over the earthquakes array. * for (let quake of data.features) { * // Reassign bigQuake if a larger * // magnitude quake is found. * if (quake.properties.mag > maxMag) { * bigQuake = quake.properties; * } * } * } * * @example * let bigQuake; * * // Load the GeoJSON and preprocess it. * async function setup() { * await loadJSON( * 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson', * handleData, * handleError * ); * * createCanvas(100, 100); * * background(200); * * // Draw a circle based on the earthquake's magnitude. * circle(50, 50, bigQuake.mag * 10); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(11); * * // Display the earthquake's location. * text(bigQuake.place, 5, 80, 100); * * describe(`A white circle on a gray background. The text "${bigQuake.place}" is written beneath the circle.`); * } * * // Find the biggest recent earthquake. * function handleData(data) { * let maxMag = 0; * // Iterate over the earthquakes array. * for (let quake of data.features) { * // Reassign bigQuake if a larger * // magnitude quake is found. * if (quake.properties.mag > maxMag) { * bigQuake = quake.properties; * } * } * } * * // Log any errors to the console. * function handleError(error) { * console.log('Oops!', error); * } */ fn.loadJSON = async function (path, successCallback, errorCallback) { // p5._validateParameters('loadJSON', arguments); try{ const { data } = await request(path, 'json'); if (successCallback) return successCallback(data); return data; } catch(err) { p5._friendlyFileLoadError(5, path); if(errorCallback) { return errorCallback(err); } else { throw err; } } }; /** * Loads a text file to create an `Array`. * * The first parameter, `path`, is always a string with the path to the file. * Paths to local files should be relative, as in * `loadStrings('assets/data.txt')`. URLs such as * `'https://example.com/data.txt'` may be blocked due to browser security. * The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) * object for more advanced usage. * * The second parameter, `successCallback`, is optional. If a function is * passed, as in `loadStrings('assets/data.txt', handleData)`, then the * `handleData()` function will be called once the data loads. The array * created from the text data will be passed to `handleData()` as its only * argument. The return value of the `handleData()` function will be used as * the final return value of `loadStrings('assets/data.txt', handleData)`. * * The third parameter, `failureCallback`, is also optional. If a function is * passed, as in `loadStrings('assets/data.txt', handleData, handleFailure)`, * then the `handleFailure()` function will be called if an error occurs while * loading. The `Error` object will be passed to `handleFailure()` as its only * argument. The return value of the `handleFailure()` function will be used as * the final return value of `loadStrings('assets/data.txt', handleData, handleFailure)`. * * This function returns a `Promise` and should be used in an `async` setup with * `await`. See the examples for the usage syntax. * * @method loadStrings * @param {String|Request} path path of the text file to be loaded. * @param {Function} [successCallback] function to call once the data is * loaded. Will be passed the array. * @param {Function} [errorCallback] function to call if the data fails to * load. Will be passed an `Error` event * object. * @return {Promise} new array containing the loaded text. * * @example * let myData; * * async function setup() { * myData = await loadStrings('assets/test.txt'); * * createCanvas(100, 100); * * background(200); * * // Select a random line from the text. * let phrase = random(myData); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display the text. * text(phrase, 10, 50, 90); * * describe(`The text "${phrase}" written in black on a gray background.`); * } * * @example * let lastLine; * * // Load the text and preprocess it. * async function setup() { * await loadStrings('assets/test.txt', handleData); * * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display the text. * text(lastLine, 10, 50, 90); * * describe('The text "I talk like an orange" written in black on a gray background.'); * } * * // Select the last line from the text. * function handleData(data) { * lastLine = data[data.length - 1]; * } * * @example * let lastLine; * * // Load the text and preprocess it. * async function setup() { * await loadStrings('assets/test.txt', handleData, handleError); * * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display the text. * text(lastLine, 10, 50, 90); * * describe('The text "I talk like an orange" written in black on a gray background.'); * } * * // Select the last line from the text. * function handleData(data) { * lastLine = data[data.length - 1]; * } * * // Log any errors to the console. * function handleError(error) { * console.error('Oops!', error); * } */ fn.loadStrings = async function (path, successCallback, errorCallback) { // p5._validateParameters('loadStrings', arguments); try{ let { data } = await request(path, 'text'); data = data.split(/\r?\n/); if (successCallback) return successCallback(data); return data; } catch(err) { p5._friendlyFileLoadError(3, path); if(errorCallback) { return errorCallback(err); } else { throw err; } } }; /** * Reads the contents of a file or URL and creates a p5.Table object with * its values. If a file is specified, it must be located in the sketch's * "data" folder. The filename parameter can also be a URL to a file found * online. By default, the file is assumed to be comma-separated (in CSV * format). Table only looks for a header row if the 'header' option is * included. * * This function returns a `Promise` and should be used in an `async` setup with * `await`. See the examples for the usage syntax. * * All files loaded and saved use UTF-8 encoding. This method is suitable for fetching files up to size of 64MB. * * @method loadTable * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :) * @param {String|Request} filename name of the file or URL to load * @param {String} [separator] the separator character used by the file, defaults to `','` * @param {String} [header] "header" to indicate table has header row * @param {Function} [callback] function to be executed after * loadTable() completes. On success, the * Table object is passed in as the * first argument. * @param {Function} [errorCallback] function to be executed if * there is an error, response is passed * in as first argument * @return {Promise} Table object containing data * * @example * // META:norender * let table; * * async function setup() { * // Create a 200x200 canvas * createCanvas(200, 200); * * // Load the CSV file with a header row * table = await loadTable('assets/mammals.csv', ',', 'header'); * * // Get the second row (index 1) * let row = table.getRow(1); * * // Set text properties * fill(0); // Set text color to black * textSize(16); // Adjust text size as needed * * // Display each column value in the row on the canvas. * // Using an offset for y-position so each value appears on a new line. * for (let c = 0; c < table.getColumnCount(); c++) { * text(row.getString(c), 10, 30 + c * 20); * } * } */ fn.loadTable = async function ( path, separator, header, successCallback, errorCallback ) { if(typeof arguments[arguments.length-1] === 'function'){ if(typeof arguments[arguments.length-2] === 'function'){ successCallback = arguments[arguments.length-2]; errorCallback = arguments[arguments.length-1]; }else { successCallback = arguments[arguments.length-1]; } } if(typeof separator !== 'string') separator = ','; if(typeof header === 'function') header = false; try{ let { data } = await request(path, 'text'); let ret = new p5.Table(); data = parse$3(data, { separator }); if(header){ ret.columns = data.shift(); }else { ret.columns = Array(data[0].length).fill(null); } data.forEach(line => { const row = new p5.TableRow(line); ret.addRow(row); }); if (successCallback) { return successCallback(ret); } else { return ret; } } catch(err) { p5._friendlyFileLoadError(2, path); if(errorCallback) { return errorCallback(err); } else { throw err; } } }; /** * Loads an XML file to create a p5.XML object. * * Extensible Markup Language * (XML) * is a standard format for sending data between applications. Like HTML, the * XML format is based on tags and attributes, as in * `<time units="s">1234</time>`. * * The first parameter, `path`, is always a string with the path to the file. * Paths to local files should be relative, as in * `loadXML('assets/data.xml')`. URLs such as `'https://example.com/data.xml'` * may be blocked due to browser security. The `path` parameter can also be defined * as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) * object for more advanced usage. * * The second parameter, `successCallback`, is optional. If a function is * passed, as in `loadXML('assets/data.xml', handleData)`, then the * `handleData()` function will be called once the data loads. The * p5.XML object created from the data will be passed * to `handleData()` as its only argument. The return value of the `handleData()` * function will be used as the final return value of `loadXML('assets/data.xml', handleData)`. * * The third parameter, `failureCallback`, is also optional. If a function is * passed, as in `loadXML('assets/data.xml', handleData, handleFailure)`, then * the `handleFailure()` function will be called if an error occurs while * loading. The `Error` object will be passed to `handleFailure()` as its only * argument. The return value of the `handleFailure()` function will be used as the * final return value of `loadXML('assets/data.xml', handleData, handleFailure)`. * * This function returns a `Promise` and should be used in an `async` setup with * `await`. See the examples for the usage syntax. * * @method loadXML * @param {String|Request} path path of the XML file to be loaded. * @param {Function} [successCallback] function to call once the data is * loaded. Will be passed the * p5.XML object. * @param {Function} [errorCallback] function to call if the data fails to * load. Will be passed an `Error` event * object. * @return {Promise} XML data loaded into a p5.XML * object. * * @example * let myXML; * * // Load the XML and create a p5.XML object. * async function setup() { * myXML = await loadXML('assets/animals.xml'); * * createCanvas(100, 100); * * background(200); * * // Get an array with all mammal tags. * let mammals = myXML.getChildren('mammal'); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(14); * * // Iterate over the mammals array. * for (let i = 0; i < mammals.length; i += 1) { * * // Calculate the y-coordinate. * let y = (i + 1) * 25; * * // Get the mammal's common name. * let name = mammals[i].getContent(); * * // Display the mammal's name. * text(name, 20, y); * } * * describe( * 'The words "Goat", "Leopard", and "Zebra" written on three separate lines. The text is black on a gray background.' * ); * } * * @example * let lastMammal; * * // Load the XML and create a p5.XML object. * async function setup() { * await loadXML('assets/animals.xml', handleData); * * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(16); * * // Display the content of the last mammal element. * text(lastMammal, 50, 50); * * describe('The word "Zebra" written in black on a gray background.'); * } * * // Get the content of the last mammal element. * function handleData(data) { * // Get an array with all mammal elements. * let mammals = data.getChildren('mammal'); * * // Get the content of the last mammal. * lastMammal = mammals[mammals.length - 1].getContent(); * } * * @example * let lastMammal; * * // Load the XML and preprocess it. * async function setup() { * await loadXML('assets/animals.xml', handleData, handleError); * * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(CENTER, CENTER); * textFont('Courier New'); * textSize(16); * * // Display the content of the last mammal element. * text(lastMammal, 50, 50); * * describe('The word "Zebra" written in black on a gray background.'); * } * * // Get the content of the last mammal element. * function handleData(data) { * // Get an array with all mammal elements. * let mammals = data.getChildren('mammal'); * * // Get the content of the last mammal. * lastMammal = mammals[mammals.length - 1].getContent(); * } * * // Log any errors to the console. * function handleError(error) { * console.error('Oops!', error); * } */ fn.loadXML = async function (path, successCallback, errorCallback) { try{ const parser = new DOMParser(); let { data } = await request(path, 'text'); const parsedDOM = parser.parseFromString(data, 'application/xml'); data = new p5.XML(parsedDOM); if (successCallback) return successCallback(data); return data; } catch(err) { p5._friendlyFileLoadError(1, path); if(errorCallback) { return errorCallback(err); } else { throw err; } } }; /** * This method is suitable for fetching files up to size of 64MB. * * @method loadBytes * @param {String|Request} file name of the file or URL to load * @param {Function} [callback] function to be executed after loadBytes() * completes * @param {Function} [errorCallback] function to be executed if there * is an error * @returns {Promise} a Uint8Array containing the loaded buffer * * @example * let data; * * async function setup() { * createCanvas(100, 100); // Create a canvas * data = await loadBytes('assets/mammals.xml'); // Load the bytes from the XML file * * background(255); // Set a white background * fill(0); // Set text color to black * * // Display the first 5 byte values on the canvas in hexadecimal format * for (let i = 0; i < 5; i++) { * let byteHex = data[i].toString(16); * text(byteHex, 10, 18 * (i + 1)); // Adjust spacing as needed * } * * describe('no image displayed, displays first 5 bytes of mammals.xml in hexadecimal format'); * } */ fn.loadBytes = async function (path, successCallback, errorCallback) { try{ let { data } = await request(path, 'arrayBuffer'); data = new Uint8Array(data); if (successCallback) return successCallback(data); return data; } catch(err) { p5._friendlyFileLoadError(6, path); if(errorCallback) { return errorCallback(err); } else { throw err; } } }; /** * Loads a file at the given path as a Blob, then returns the resulting data or * passes it to a success callback function, if provided. On load, this function * returns a `Promise` that resolves to a Blob containing the file data. * * @method loadBlob * @param {String|Request} path - The path or Request object pointing to the file * you want to load. * @param {Function} [successCallback] - Optional. A function to be called if the * file successfully loads, receiving the * resulting Blob as its only argument. * @param {Function} [errorCallback] - Optional. A function to be called if an * error occurs during loading; receives the * error object as its only argument. * @returns {Promise} A promise that resolves with the loaded Blob. * * @example * let myBlob; * * async function setup() { * createCanvas(200, 200); * background(220); * try { * // 1. Load an image file as a Blob. * myBlob = await loadBlob('assets/flower-1.png'); * * // 2. Convert the Blob into an object URL. * const objectUrl = URL.createObjectURL(myBlob); * * // 3. Load that object URL into a p5.Image. * loadImage(objectUrl, (img) => { * // 4. Display the loaded image. * image(img, 0, 0, width, height); * }); * } catch (err) { * console.error('Error loading blob:', err); * } * } */ fn.loadBlob = async function(path, successCallback, errorCallback) { try{ const { data } = await request(path, 'blob'); if (successCallback) return successCallback(data); return data; } catch(err) { if(errorCallback) { return errorCallback(err); } else { throw err; } } }; /** * Method for executing an HTTP GET request. If data type is not specified, * it will default to `'text'`. This is equivalent to * calling httpDo(path, 'GET'). The 'binary' datatype will return * a Blob object, and the 'arrayBuffer' datatype will return an ArrayBuffer * which can be used to initialize typed arrays (such as Uint8Array). * * @method httpGet * @param {String|Request} path name of the file or url to load * @param {String} [datatype] "json", "jsonp", "binary", "arrayBuffer", * "xml", or "text" * @param {Function} [callback] function to be executed after * httpGet() completes, data is passed in * as first argument * @param {Function} [errorCallback] function to be executed if * there is an error, response is passed * in as first argument * @return {Promise} A promise that resolves with the data when the operation * completes successfully or rejects with the error after * one occurs. * @example * // META:norender * // Examples use USGS Earthquake API: * // https://earthquake.usgs.gov/fdsnws/event/1/#methods * let earthquakes; * async function setup() { * // Get the most recent earthquake in the database * let url = * 'https://earthquake.usgs.gov/fdsnws/event/1/query?' + * 'format=geojson&limit=1&orderby=time'; * earthquakes = await httpGet(url, 'json'); * } * * function draw() { * if (!earthquakes) { * // Wait until the earthquake data has loaded before drawing. * return; * } * background(200); * // Get the magnitude and name of the earthquake out of the loaded JSON * let earthquakeMag = earthquakes.features[0].properties.mag; * let earthquakeName = earthquakes.features[0].properties.place; * ellipse(width / 2, height / 2, earthquakeMag * 10, earthquakeMag * 10); * textAlign(CENTER); * text(earthquakeName, 0, height - 30, width, 30); * noLoop(); * } */ /** * @method httpGet * @param {String|Request} path * @param {Function} callback * @param {Function} [errorCallback] * @return {Promise} */ fn.httpGet = async function (path, datatype='text', successCallback, errorCallback) { // p5._validateParameters('httpGet', arguments); if (typeof datatype === 'function') { errorCallback = successCallback; successCallback = datatype; datatype = 'text'; } // This is like a more primitive version of the other load functions. // If the user wanted to customize more behavior, pass in Request to path. return this.httpDo(path, 'GET', datatype, successCallback, errorCallback); }; /** * Method for executing an HTTP POST request. If data type is not specified, * it will default to `'text'`. This is equivalent to * calling httpDo(path, 'POST'). * * @method httpPost * @param {String|Request} path name of the file or url to load * @param {Object|Boolean} [data] param data passed sent with request * @param {String} [datatype] "json", "jsonp", "xml", or "text". * If omitted, httpPost() will guess. * @param {Function} [callback] function to be executed after * httpPost() completes, data is passed in * as first argument * @param {Function} [errorCallback] function to be executed if * there is an error, response is passed * in as first argument * @return {Promise} A promise that resolves with the data when the operation * completes successfully or rejects with the error after * one occurs. * * @example * // Examples use jsonplaceholder.typicode.com for a Mock Data API * * let url = 'https://jsonplaceholder.typicode.com/posts'; * let postData = { userId: 1, title: 'p5 Clicked!', body: 'p5.js is very cool.' }; * * function setup() { * createCanvas(100, 100); * background(200); * } * * function mousePressed() { * httpPost(url, postData, 'json', function(result) { * strokeWeight(2); * text(result.body, mouseX, mouseY); * }); * } * * @example * let url = 'ttps://invalidURL'; // A bad URL that will cause errors * let postData = { title: 'p5 Clicked!', body: 'p5.js is very cool.' }; * * function setup() { * createCanvas(100, 100); * background(200); * } * * function mousePressed() { * httpPost( * url, * postData, * 'json', * function(result) { * // ... won't be called * }, * function(error) { * strokeWeight(2); * text(error.toString(), mouseX, mouseY); * } * ); * } */ /** * @method httpPost * @param {String|Request} path * @param {Object|Boolean} data * @param {Function} [callback] * @param {Function} [errorCallback] * @return {Promise} */ /** * @method httpPost * @param {String|Request} path * @param {Function} [callback] * @param {Function} [errorCallback] * @return {Promise} */ fn.httpPost = async function (path, data, datatype='text', successCallback, errorCallback) { // p5._validateParameters('httpPost', arguments); // This behave similarly to httpGet and additional options should be passed // as a `Request`` to path. Both method and body will be overridden. // Will try to infer correct Content-Type for given data. if (typeof data === 'function') { // Assume both data and datatype are functions as data should not be function successCallback = data; errorCallback = datatype; data = undefined; datatype = 'text'; } else if (typeof datatype === 'function') { // Data is provided but not datatype\ errorCallback = successCallback; successCallback = datatype; datatype = 'text'; } let reqData = data; let contentType = 'text/plain'; // Normalize data if(data instanceof p5.XML) { reqData = data.serialize(); contentType = 'application/xml'; } else if(data instanceof p5.Image) { reqData = await data.toBlob(); contentType = 'image/png'; } else if (typeof data === 'object') { reqData = JSON.stringify(data); contentType = 'application/json'; } const requestOptions = { method: 'POST', body: reqData, headers: { 'Content-Type': contentType } }; if (reqData) { requestOptions.body = reqData; } const req = new Request(path, requestOptions); return this.httpDo(req, 'POST', datatype, successCallback, errorCallback); }; /** * Method for executing an HTTP request. If data type is not specified, * it will default to `'text'`. * * This function is meant for more advanced usage of HTTP requests in p5.js. It is * best used when a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) * object is passed to the `path` parameter. * * This method is suitable for fetching files up to size of 64MB when "GET" is used. * * @method httpDo * @param {String|Request} path name of the file or url to load * @param {String} [method] either "GET", "POST", "PUT", "DELETE", * or other HTTP request methods * @param {String} [datatype] "json", "jsonp", "xml", or "text" * @param {Object} [data] param data passed sent with request * @param {Function} [callback] function to be executed after * httpGet() completes, data is passed in * as first argument * @param {Function} [errorCallback] function to be executed if * there is an error, response is passed * in as first argument * @return {Promise} A promise that resolves with the data when the operation * completes successfully or rejects with the error after * one occurs. * * @example * // Examples use USGS Earthquake API: * // https://earthquake.usgs.gov/fdsnws/event/1/#methods * * // displays an animation of all USGS earthquakes * let earthquakes; * let eqFeatureIndex = 0; * * function setup() { * createCanvas(100,100); * * let url = 'https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson'; * * const req = new Request(url, { * method: 'GET', * headers: {authorization: 'Bearer secretKey'} * }); * // httpDo(path, method, datatype, success, error) * * httpDo( * req, * 'GET', * 'json', * res => { * earthquakes = res; * }, * err => { * console.error('Error loading data:', err); * } * ); * } * * function draw() { * // wait until the data is loaded * if (!earthquakes || !earthquakes.features[eqFeatureIndex]) { * return; * } * clear(); * * let feature = earthquakes.features[eqFeatureIndex]; * let mag = feature.properties.mag; * let rad = mag / 11 * ((width + height) / 2); * fill(255, 0, 0, 100); * ellipse(width / 2 + random(-2, 2), height / 2 + random(-2, 2), rad, rad); * * if (eqFeatureIndex >= earthquakes.features.length) { * eqFeatureIndex = 0; * } else { * eqFeatureIndex += 1; * } * } */ /** * @method httpDo * @param {String|Request} path * @param {Function} [callback] * @param {Function} [errorCallback] * @return {Promise} */ fn.httpDo = async function ( path, method, datatype, successCallback, errorCallback ) { // This behave similarly to httpGet but even more primitive. The user // will most likely want to pass in a Request to path, the only convenience // is that datatype will be taken into account to parse the response. if(typeof datatype === 'function'){ errorCallback = successCallback; successCallback = datatype; datatype = undefined; } // Try to infer data type if it is defined if(!datatype){ const extension = typeof path === 'string' ? path.split('.').pop() : path.url.split('.').pop(); switch(extension) { case 'json': datatype = 'json'; break; case 'jpg': case 'jpeg': case 'png': case 'webp': case 'gif': datatype = 'blob'; break; case 'xml': // NOTE: still need to normalize type handling/mapping // datatype = 'xml'; case 'txt': default: datatype = 'text'; } } const req = new Request(path, { method }); try{ const { data } = await request(req, datatype); if (successCallback) { return successCallback(data); } else { return data; } } catch(err) { if(errorCallback) { return errorCallback(err); } else { throw err; } } }; /** * @module IO * @submodule Output * @for p5 */ // private array of p5.PrintWriter objects fn._pWriters = []; /** * Creates a new p5.PrintWriter object. * * p5.PrintWriter objects provide a way to * save a sequence of text data, called the *print stream*, to the user's * computer. They're low-level objects that enable precise control of text * output. Functions such as * saveStrings() and * saveJSON() are easier to use for simple file * saving. * * The first parameter, `filename`, is the name of the file to be written. If * a string is passed, as in `createWriter('words.txt')`, a new * p5.PrintWriter object will be created that * writes to a file named `words.txt`. * * The second parameter, `extension`, is optional. If a string is passed, as * in `createWriter('words', 'csv')`, the first parameter will be interpreted * as the file name and the second parameter as the extension. * * @method createWriter * @param {String} name name of the file to create. * @param {String} [extension] format to use for the file. * @return {p5.PrintWriter} stream for writing data. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create a p5.PrintWriter object. * let myWriter = createWriter('xo.txt'); * * // Add some lines to the print stream. * myWriter.print('XOO'); * myWriter.print('OXO'); * myWriter.print('OOX'); * * // Save the file and close the print stream. * myWriter.close(); * } * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create a p5.PrintWriter object. * // Use the file format .csv. * let myWriter = createWriter('mauna_loa_co2', 'csv'); * * // Add some lines to the print stream. * myWriter.print('date,ppm_co2'); * myWriter.print('1960-01-01,316.43'); * myWriter.print('1970-01-01,325.06'); * myWriter.print('1980-01-01,337.9'); * myWriter.print('1990-01-01,353.86'); * myWriter.print('2000-01-01,369.45'); * myWriter.print('2020-01-01,413.61'); * * // Save the file and close the print stream. * myWriter.close(); * } * } */ fn.createWriter = function (name, extension) { let newPW; // check that it doesn't already exist for (const i in fn._pWriters) { if (fn._pWriters[i].name === name) { // if a p5.PrintWriter w/ this name already exists... // return fn._pWriters[i]; // return it w/ contents intact. // or, could return a new, empty one with a unique name: newPW = new p5.PrintWriter(name + this.millis(), extension); fn._pWriters.push(newPW); return newPW; } } newPW = new p5.PrintWriter(name, extension); fn._pWriters.push(newPW); return newPW; }; /** * A class to describe a print stream. * * Each `p5.PrintWriter` object provides a way to save a sequence of text * data, called the *print stream*, to the user's computer. It's a low-level * object that enables precise control of text output. Functions such as * saveStrings() and * saveJSON() are easier to use for simple file * saving. * * Note: createWriter() is the recommended way * to make an instance of this class. * * @class p5.PrintWriter * @param {String} filename name of the file to create. * @param {String} [extension] format to use for the file. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * // Create a p5.PrintWriter object. * let myWriter = createWriter('xo.txt'); * * // Add some lines to the print stream. * myWriter.print('XOO'); * myWriter.print('OXO'); * myWriter.print('OOX'); * * // Save the file and close the print stream. * myWriter.close(); * } */ p5.PrintWriter = function (filename, extension) { let self = this; this.name = filename; this.content = ''; /** * Writes data to the print stream without adding new lines. * * The parameter, `data`, is the data to write. `data` can be a number or * string, as in `myWriter.write('hi')`, or an array of numbers and strings, * as in `myWriter.write([1, 2, 3])`. A comma will be inserted between array * array elements when they're added to the print stream. * * @method write * @param {String|Number|Array} data data to be written as a string, number, * or array of strings and numbers. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * // Create a p5.PrintWriter object. * let myWriter = createWriter('numbers.txt'); * * // Add some data to the print stream. * myWriter.write('1,2,3,'); * myWriter.write(['4', '5', '6']); * * // Save the file and close the print stream. * myWriter.close(); * } */ this.write = function (data) { this.content += data; }; /** * Writes data to the print stream with new lines added. * * The parameter, `data`, is the data to write. `data` can be a number or * string, as in `myWriter.print('hi')`, or an array of numbers and strings, * as in `myWriter.print([1, 2, 3])`. A comma will be inserted between array * array elements when they're added to the print stream. * * @method print * @param {String|Number|Array} data data to be written as a string, number, * or array of strings and numbers. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * // Create a p5.PrintWriter object. * let myWriter = createWriter('numbers.txt'); * * // Add some data to the print stream. * myWriter.print('1,2,3,'); * myWriter.print(['4', '5', '6']); * * // Save the file and close the print stream. * myWriter.close(); * } */ this.print = function (data) { this.content += `${data}\n`; }; /** * Clears all data from the print stream. * * @method clear * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * // Create a p5.PrintWriter object. * let myWriter = createWriter('numbers.txt'); * * // Add some data to the print stream. * myWriter.print('Hello p5*js!'); * * // Clear the print stream. * myWriter.clear(); * * // Save the file and close the print stream. * myWriter.close(); * } */ this.clear = function () { this.content = ''; }; /** * Saves the file and closes the print stream. * * @method close * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * // Create a p5.PrintWriter object. * let myWriter = createWriter('cat.txt'); * * // Add some data to the print stream. * // ASCII art courtesy Wikipedia: * // https://en.wikipedia.org/wiki/ASCII_art * myWriter.print(' (\\_/) '); * myWriter.print("(='.'=)"); * myWriter.print('(")_(")'); * * // Save the file and close the print stream. * myWriter.close(); * } */ this.close = function () { // convert String to Array for the writeFile Blob const arr = []; arr.push(this.content); fn.writeFile(arr, filename, extension); // remove from _pWriters array and delete self for (const i in fn._pWriters) { if (fn._pWriters[i].name === this.name) { // remove from _pWriters array fn._pWriters.splice(i, 1); } } self.clear(); self = {}; }; }; /** * @module IO * @submodule Output * @for p5 */ // object, filename, options --> saveJSON, saveStrings, // filename, [extension] [canvas] --> saveImage /** * Saves a given element(image, text, json, csv, wav, or html) to the client's * computer. The first parameter can be a pointer to element we want to save. * The element can be one of p5.Element,an Array of * Strings, an Array of JSON, a JSON object, a p5.Table * , a p5.Image, or a p5.SoundFile (requires * p5.sound). The second parameter is a filename (including extension).The * third parameter is for options specific to this type of object. This method * will save a file that fits the given parameters. * If it is called without specifying an element, by default it will save the * whole canvas as an image file. You can optionally specify a filename as * the first parameter in such a case. * **Note that it is not recommended to * call this method within draw, as it will open a new save dialog on every * render.** * * @method save * @param {Object|String} [objectOrFilename] If filename is provided, will * save canvas as an image with * either png or jpg extension * depending on the filename. * If object is provided, will * save depending on the object * and filename (see examples * above). * @param {String} [filename] If an object is provided as the first * parameter, then the second parameter * indicates the filename, * and should include an appropriate * file extension (see examples above). * @param {Boolean|String} [options] Additional options depend on * filetype. For example, when saving JSON, * true indicates that the * output will be optimized for filesize, * rather than readability. * * @example * // META:norender * // Saves the canvas as an image * cnv = createCanvas(300, 300); * save(cnv, 'myCanvas.jpg'); * * // Saves the canvas as an image by default * save('myCanvas.jpg'); * describe('An example for saving a canvas as an image.'); * * @example * // META:norender * // Saves p5.Image as an image * img = createImage(10, 10); * save(img, 'myImage.png'); * describe('An example for saving a p5.Image element as an image.'); * * @example * // META:norender * // Saves p5.Renderer object as an image * obj = createGraphics(100, 100); * save(obj, 'myObject.png'); * describe('An example for saving a p5.Renderer element.'); * * @example * // META:norender * let myTable = new p5.Table(); * // Saves table as html file * save(myTable, 'myTable.html'); * * // Comma Separated Values * save(myTable, 'myTable.csv'); * * // Tab Separated Values * save(myTable, 'myTable.tsv'); * * describe(`An example showing how to save a table in formats of * HTML, CSV and TSV.`); * * @example * // META:norender * let myJSON = { a: 1, b: true }; * * // Saves pretty JSON * save(myJSON, 'my.json'); * * // Optimizes JSON filesize * save(myJSON, 'my.json', true); * * describe('An example for saving JSON to a txt file with some extra arguments.'); * * @example * // META:norender * // Saves array of strings to text file with line breaks after each item * let arrayOfStrings = ['a', 'b']; * save(arrayOfStrings, 'my.txt'); * describe(`An example for saving an array of strings to text file * with line breaks.`); */ fn.save = function (object, _filename, _options) { // TODO: parameters is not used correctly // parse the arguments and figure out which things we are saving const args = arguments; // ================================================= // OPTION 1: saveCanvas... // if no arguments are provided, save canvas const cnv = this._curElement ? this._curElement.elt : this.elt; if (args.length === 0) { fn.saveCanvas(cnv); return; } else if (args[0] instanceof Renderer || args[0] instanceof Graphics) { // otherwise, parse the arguments // if first param is a p5Graphics, then saveCanvas fn.saveCanvas(args[0].canvas, args[1], args[2]); return; } else if (args.length === 1 && typeof args[0] === 'string') { // if 1st param is String and only one arg, assume it is canvas filename fn.saveCanvas(cnv, args[0]); } else { // ================================================= // OPTION 2: extension clarifies saveStrings vs. saveJSON const extension = _checkFileExtension(args[1], args[2])[1]; switch (extension) { case 'json': fn.saveJSON(args[0], args[1], args[2]); return; case 'txt': fn.saveStrings(args[0], args[1], args[2]); return; // ================================================= // OPTION 3: decide based on object... default: if (args[0] instanceof Array) { fn.saveStrings(args[0], args[1], args[2]); } else if (args[0] instanceof p5.Table) { fn.saveTable(args[0], args[1], args[2]); } else if (args[0] instanceof p5.Image) { fn.saveCanvas(args[0].canvas, args[1]); } else if (args[0] instanceof p5.SoundFile) { fn.saveSound(args[0], args[1], args[2], args[3]); } } } }; /** * Saves an `Object` or `Array` to a JSON file. * * JavaScript Object Notation * (JSON) * is a standard format for sending data between applications. The format is * based on JavaScript objects which have keys and values. JSON files store * data in an object with strings as keys. Values can be strings, numbers, * Booleans, arrays, `null`, or other objects. * * The first parameter, `json`, is the data to save. The data can be an array, * as in `[1, 2, 3]`, or an object, as in * `{ x: 50, y: 50, color: 'deeppink' }`. * * The second parameter, `filename`, is a string that sets the file's name. * For example, calling `saveJSON([1, 2, 3], 'data.json')` saves the array * `[1, 2, 3]` to a file called `data.json` on the user's computer. * * The third parameter, `optimize`, is optional. If `true` is passed, as in * `saveJSON([1, 2, 3], 'data.json', true)`, then all unneeded whitespace will * be removed to reduce the file size. * * Note: The browser will either save the file immediately or prompt the user * with a dialogue window. * * @method saveJSON * @param {Array|Object} json data to save. * @param {String} filename name of the file to be saved. * @param {Boolean} [optimize] whether to trim unneeded whitespace. Defaults * to `true`. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create an array. * let data = [1, 2, 3]; * * // Save the JSON file. * saveJSON(data, 'numbers.json'); * } * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create an object. * let data = { x: mouseX, y: mouseY }; * * // Save the JSON file. * saveJSON(data, 'state.json'); * } * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create an object. * let data = { x: mouseX, y: mouseY }; * * // Save the JSON file and reduce its size. * saveJSON(data, 'state.json', true); * } * } */ fn.saveJSON = function (json, filename, optimize) { // p5._validateParameters('saveJSON', arguments); let stringify; if (optimize) { stringify = JSON.stringify(json); } else { stringify = JSON.stringify(json, undefined, 2); } this.saveStrings(stringify.split('\n'), filename, 'json'); }; /** * Saves an `Array` of `String`s to a file, one per line. * * The first parameter, `list`, is an array with the strings to save. * * The second parameter, `filename`, is a string that sets the file's name. * For example, calling `saveStrings(['0', '01', '011'], 'data.txt')` saves * the array `['0', '01', '011']` to a file called `data.txt` on the user's * computer. * * The third parameter, `extension`, is optional. If a string is passed, as in * `saveStrings(['0', '01', '0`1'], 'data', 'txt')`, the second parameter will * be interpreted as the file name and the third parameter as the extension. * * The fourth parameter, `isCRLF`, is also optional, If `true` is passed, as * in `saveStrings(['0', '01', '011'], 'data', 'txt', true)`, then two * characters, `\r\n` , will be added to the end of each string to create new * lines in the saved file. `\r` is a carriage return (CR) and `\n` is a line * feed (LF). By default, only `\n` (line feed) is added to each string in * order to create new lines. * * Note: The browser will either save the file immediately or prompt the user * with a dialogue window. * * @method saveStrings * @param {String[]} list data to save. * @param {String} filename name of file to be saved. * @param {String} [extension] format to use for the file. * @param {Boolean} [isCRLF] whether to add `\r\n` to the end of each * string. Defaults to `false`. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create an array. * let data = ['0', '01', '011']; * * // Save the text file. * saveStrings(data, 'data.txt'); * } * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create an array. * // ASCII art courtesy Wikipedia: * // https://en.wikipedia.org/wiki/ASCII_art * let data = [' (\\_/) ', "(='.'=)", '(")_(")']; * * // Save the text file. * saveStrings(data, 'cat', 'txt'); * } * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(LEFT, CENTER); * textFont('Courier New'); * textSize(12); * * // Display instructions. * text('Double-click to save', 5, 50, 90); * * describe('The text "Double-click to save" written in black on a gray background.'); * } * * // Save the file when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * // Create an array. * // +--+ * // / /| * // +--+ + * // | |/ * // +--+ * let data = [' +--+', ' / /|', '+--+ +', '| |/', '+--+']; * * // Save the text file. * // Use CRLF for line endings. * saveStrings(data, 'box', 'txt', true); * } * } */ fn.saveStrings = function (list, filename, extension, isCRLF) { // p5._validateParameters('saveStrings', arguments); const ext = extension || 'txt'; const pWriter = new p5.PrintWriter(filename, ext); for (let item of list) { isCRLF ? pWriter.write(item + '\r\n') : pWriter.write(item + '\n'); } pWriter.close(); pWriter.clear(); }; // ======= // HELPERS // ======= function escapeHelper(content) { return content .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Writes the contents of a Table object to a file. Defaults to a * text file with comma-separated-values ('csv') but can also * use tab separation ('tsv'), or generate an HTML table ('html'). * The file saving process and location of the saved file will * vary between web browsers. * * @method saveTable * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :) * @param {p5.Table} Table the Table object to save to a file * @param {String} filename the filename to which the Table should be saved * @param {String} [options] can be one of "tsv", "csv", or "html" * @example * let table; * * function setup() { * table = new p5.Table(); * * table.addColumn('id'); * table.addColumn('species'); * table.addColumn('name'); * * let newRow = table.addRow(); * newRow.setNum('id', table.getRowCount() - 1); * newRow.setString('species', 'Panthera leo'); * newRow.setString('name', 'Lion'); * * // To save, un-comment next line then click 'run' * // saveTable(table, 'new.csv'); * * describe('no image displayed'); * } * * // Saves the following to a file called 'new.csv': * // id,species,name * // 0,Panthera leo,Lion */ fn.saveTable = function (table, filename, options) { // p5._validateParameters('saveTable', arguments); let ext; if (options === undefined) { ext = filename.substring(filename.lastIndexOf('.') + 1, filename.length); if(ext === filename) ext = 'csv'; } else { ext = options; } const pWriter = this.createWriter(filename, ext); const header = table.columns; let sep = ','; // default to CSV if (ext === 'tsv') { sep = '\t'; } if (ext !== 'html') { const output = table.toString(sep); pWriter.write(output); } else { // otherwise, make HTML pWriter.print(''); pWriter.print(''); let str = ' '); pWriter.print(''); pWriter.print(' '); // make header if it has values if (header[0] !== '0') { pWriter.print(' '); for (let k = 0; k < header.length; k++) { const e = escapeHelper(header[k]); pWriter.print(` '); } pWriter.print(' '); } // make rows for (let row = 0; row < table.rows.length; row++) { pWriter.print(' '); for (let col = 0; col < table.columns.length; col++) { const entry = table.rows[row].getString(col); const htmlEntry = escapeHelper(entry); pWriter.print(` '); } pWriter.print(' '); } pWriter.print('
${e}`); pWriter.print('
${htmlEntry}`); pWriter.print('
'); pWriter.print(''); pWriter.print(''); } // close and clear the pWriter pWriter.close(); pWriter.clear(); }; // end saveTable() /** * Generate a blob of file data as a url to prepare for download. * Accepts an array of data, a filename, and an extension (optional). * This is a private function because it does not do any formatting, * but it is used by saveStrings, saveJSON, saveTable etc. * * @param {Array} dataToDownload * @param {String} filename * @param {String} [extension] * @private */ fn.writeFile = function (dataToDownload, filename, extension) { let type = 'application/octet-stream'; if (fn._isSafari()) { type = 'text/plain'; } const blob = new Blob(dataToDownload, { type }); fn.downloadFile(blob, filename, extension); }; /** * Forces download. Accepts a url to filedata/blob, a filename, * and an extension (optional). * This is a private function because it does not do any formatting, * but it is used by saveStrings, saveJSON, saveTable etc. * * @method downloadFile * @private * @param {String|Blob} data either an href generated by createObjectURL, * or a Blob object containing the data * @param {String} [filename] * @param {String} [extension] */ fn.downloadFile = downloadFile; /** * Returns a file extension, or another string * if the provided parameter has no extension. * * @param {String} filename * @param {String} [extension] * @return {String[]} [fileName, fileExtension] * * @private */ fn._checkFileExtension = _checkFileExtension; /** * Returns true if the browser is Safari, false if not. * Safari makes trouble for downloading files. * * @return {Boolean} [description] * @private */ fn._isSafari = function () { // The following line is CC BY SA 3 by user Fregante https://stackoverflow.com/a/23522755 return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); }; } if(typeof p5 !== 'undefined'){ files(p5, p5.prototype); } var X={trailer:59};function F(t=256){let e=0,s=new Uint8Array(t);return {get buffer(){return s.buffer},reset(){e=0;},bytesView(){return s.subarray(0,e)},bytes(){return s.slice(0,e)},writeByte(r){n(e+1),s[e]=r,e++;},writeBytes(r,o=0,i=r.length){n(e+i);for(let c=0;c=r)return;var i=1024*1024;r=Math.max(r,o*(o>>0),o!=0&&(r=Math.max(r,256));let c=s;s=new Uint8Array(r),e>0&&s.set(c.subarray(0,e),0);}}var O=12,J=5003,lt=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function at(t,e,s,n,r=F(512),o=new Uint8Array(256),i=new Int32Array(J),c=new Int32Array(J)){let x=i.length,a=Math.max(2,n);o.fill(0),c.fill(0),i.fill(-1);let l=0,f=0,g=a+1,h=g,b=false,w=h,_=(1<=0;)if(M-=V,M<0&&(M+=x),i[M]===v){A=c[M];break t}I(A),A=m,B<1<0?l|=y<=8;)o[p++]=l&255,p>=254&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0),l>>=8,f-=8;if((B>_||b)&&(b?(w=h,_=(1<0;)o[p++]=l&255,p>=254&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0),l>>=8,f-=8;p>0&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0);}}}var $=at;function D(t,e,s){return t<<8&63488|e<<2&992|s>>3}function G(t,e,s,n){return t>>4|e&240|(s&240)<<4|(n&240)<<8}function j(t,e,s){return t>>4<<8|e&240|s>>4}function R(t,e,s){return ts?s:t}function T(t){return t*t}function tt(t,e,s){var n=0,r=1e100;let o=t[e],i=o.cnt,x=o.rc,a=o.gc,l=o.bc;for(var f=o.fw;f!=0;f=t[f].fw){let h=t[f],b=h.cnt,w=i*b/(i+b);if(!(w>=r)){var g=0;(g+=w*T(h.rc-x),!(g>=r)&&(g+=w*T(h.gc-a),!(g>=r)&&(g+=w*T(h.bc-l),!(g>=r)&&(r=g,n=f))));}}o.err=r,o.nn=n;}function Q(){return {ac:0,rc:0,gc:0,bc:0,cnt:0,nn:0,fw:0,bk:0,tm:0,mtm:0,err:0}}function ut(t,e){let s=e==="rgb444"?4096:65536,n=new Array(s),r=t.length;if(e==="rgba4444")for(let o=0;o>24&255,x=i>>16&255,a=i>>8&255,l=i&255,f=G(l,a,x,c),g=f in n?n[f]:n[f]=Q();g.rc+=l,g.gc+=a,g.bc+=x,g.ac+=c,g.cnt++;}else if(e==="rgb444")for(let o=0;o>16&255,x=i>>8&255,a=i&255,l=j(a,x,c),f=l in n?n[l]:n[l]=Q();f.rc+=a,f.gc+=x,f.bc+=c,f.cnt++;}else for(let o=0;o>16&255,x=i>>8&255,a=i&255,l=D(a,x,c),f=l in n?n[l]:n[l]=Q();f.rc+=a,f.gc+=x,f.bc+=c,f.cnt++;}return n}function H(t,e,s={}){let{format:n="rgb565",clearAlpha:r=true,clearAlphaColor:o=0,clearAlphaThreshold:i=0,oneBitAlpha:c=false}=s;if(!t||!t.buffer)throw new Error("quantize() expected RGBA Uint8Array data");if(!(t instanceof Uint8Array)&&!(t instanceof Uint8ClampedArray))throw new Error("quantize() expected RGBA Uint8Array data");let x=new Uint32Array(t.buffer),a=s.useSqrt!==false,l=n==="rgba4444",f=ut(x,n),g=f.length,h=g-1,b=new Uint32Array(g+1);for(var w=0,u=0;u1&&(p=B>>1,!(f[k=b[p]].err<=A));B=p)b[B]=k;b[B]=u;}var z=w-e;for(u=0;u=d.mtm&&f[d.nn].mtm<=d.tm)break;d.mtm==h?I=b[1]=b[b[0]--]:(tt(f,I),d.tm=u);var A=f[I].err;for(B=1;(p=B+B)<=b[0]&&(pf[b[p+1]].err&&p++,!(A<=f[k=b[p]].err));B=p)b[B]=k;b[B]=I;}var y=f[d.nn],m=d.cnt,v=y.cnt,_=1/(m+v);l&&(d.ac=_*(m*d.ac+v*y.ac)),d.rc=_*(m*d.rc+v*y.rc),d.gc=_*(m*d.gc+v*y.gc),d.bc=_*(m*d.bc+v*y.bc),d.cnt+=y.cnt,d.mtm=++u,f[y.bk].fw=y.fw,f[y.fw].bk=y.bk,y.mtm=h;}let M=[];var V=0;for(u=0;;++V){let L=R(Math.round(f[u].rc),0,255),C=R(Math.round(f[u].gc),0,255),Y=R(Math.round(f[u].bc),0,255),E=255;if(l){if(E=R(Math.round(f[u].ac),0,255),c){let st=typeof c=="number"?c:127;E=E<=st?0:255;}r&&E<=i&&(L=C=Y=o,E=0);}let K=l?[L,C,Y,E]:[L,C,Y];if(xt(M,K)||M.push(K),(u=f[u].fw)==0)break}return M}function xt(t,e){for(let s=0;s=4&&e.length>=4?n[3]===e[3]:true;if(r&&o)return true}return false}function U(t,e){var s=0,n;for(n=0;n=0&&dt(n,k);}let z=Math.round(_/10);wt(n,p,z,b,w);let d=Boolean(u)&&!A;ht(n,f,g,d?u:null),d&&it(n,u),yt(n,l,f,g,B,o,i,c);}};function a(){ft(n,"GIF89a");}}function wt(t,e,s,n,r){t.writeByte(33),t.writeByte(249),t.writeByte(4),r<0&&(r=0,n=false);var o,i;n?(o=1,i=2):(o=0,i=0),e>=0&&(i=e&7),i<<=2;let c=0;t.writeByte(0|i|c|o),S(t,s),t.writeByte(r||0),t.writeByte(0);}function pt(t,e,s,n,r=8){let o=1,i=0,c=Z(n.length)-1,x=o<<7|r-1<<4|i<<3|c,a=0,l=0;S(t,e),S(t,s),t.writeBytes([x,a,l]);}function dt(t,e){t.writeByte(33),t.writeByte(255),t.writeByte(11),ft(t,"NETSCAPE2.0"),t.writeByte(3),t.writeByte(1),S(t,e),t.writeByte(0);}function it(t,e){let s=1<>8&255);}function ft(t,e){for(var s=0;sp5.Image object. * * `loadImage()` interprets the first parameter one of three ways. If the path * to an image file is provided, `loadImage()` will load it. Paths to local * files should be relative, such as `'assets/thundercat.jpg'`. URLs such as * `'https://example.com/thundercat.jpg'` may be blocked due to browser * security. Raw image data can also be passed as a base64 encoded image in * the form `'data:image/png;base64,arandomsequenceofcharacters'`. The `path` * parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) * object for more advanced usage. * * The second parameter is optional. If a function is passed, it will be * called once the image has loaded. The callback function can optionally use * the new p5.Image object. The return value of the * function will be used as the final return value of `loadImage()`. * * The third parameter is also optional. If a function is passed, it will be * called if the image fails to load. The callback function can optionally use * the event error. The return value of the function will be used as the final * return value of `loadImage()`. * * This function returns a `Promise` and should be used in an `async` setup with * `await`. See the examples for the usage syntax. * * @method loadImage * @param {String|Request} path path of the image to be loaded or base64 encoded image. * @param {function(p5.Image)} [successCallback] function called with * p5.Image once it * loads. * @param {function(Event)} [failureCallback] function called with event * error if the image fails to load. * @return {Promise} the p5.Image object. * * @example * let img; * * // Load the image and create a p5.Image object. * async function setup() { * img = await loadImage('assets/laDefense.jpg'); * createCanvas(100, 100); * * // Draw the image. * image(img, 0, 0); * * describe('Image of the underside of a white umbrella and a gridded ceiling.'); * } * * @example * async function setup() { * // Call handleImage() once the image loads. * await loadImage('assets/laDefense.jpg', handleImage); * * describe('Image of the underside of a white umbrella and a gridded ceiling.'); * } * * // Display the image. * function handleImage(img) { * image(img, 0, 0); * } * * @example * async function setup() { * // Call handleImage() once the image loads or * // call handleError() if an error occurs. * await loadImage('assets/laDefense.jpg', handleImage, handleError); * } * * // Display the image. * function handleImage(img) { * image(img, 0, 0); * * describe('Image of the underside of a white umbrella and a gridded ceiling.'); * } * * // Log the error. * function handleError(event) { * console.error('Oops!', event); * } */ fn.loadImage = async function( path, successCallback, failureCallback ) { // p5._validateParameters('loadImage', arguments); try{ let pImg = new p5.Image(1, 1, this); const req = new Request(path, { method: 'GET', mode: 'cors' }); const { data, headers } = await request(req, 'bytes'); // GIF section const contentType = headers.get('content-type'); if (contentType === null) { console.warn( 'The image you loaded does not have a Content-Type header. If you are using the online editor consider reuploading the asset.' ); } if (contentType && contentType.includes('image/gif')) { await _createGif( data, pImg ); } else { // Non-GIF Section const blob = new Blob([data]); const img = await createImageBitmap(blob); pImg.width = pImg.canvas.width = img.width; pImg.height = pImg.canvas.height = img.height; // Draw the image into the backing canvas of the p5.Image pImg.drawingContext.drawImage(img, 0, 0); } pImg.modified = true; if(successCallback){ return successCallback(pImg); }else { return pImg; } } catch(err) { p5._friendlyFileLoadError(0, path); if (typeof failureCallback === 'function') { return failureCallback(err); } else { throw err; } } }; /** * Generates a gif from a sketch and saves it to a file. * * `saveGif()` may be called in setup() or at any * point while a sketch is running. * * The first parameter, `fileName`, sets the gif's file name. * * The second parameter, `duration`, sets the gif's duration in seconds. * * The third parameter, `options`, is optional. If an object is passed, * `saveGif()` will use its properties to customize the gif. `saveGif()` * recognizes the properties `delay`, `units`, `silent`, * `notificationDuration`, and `notificationID`. * * @method saveGif * @param {String} filename file name of gif. * @param {Number} duration duration in seconds to capture from the sketch. * @param {Object} [options] an object that can contain five more properties: * @param {Number} [options.delay=0] How much time to wait before recording. * @param {'seconds'|'frames'} [options.units='seconds'] The units of the duration and delay. * @param {Boolean} [options.silent=false] Whether to show progress notifications. * @param {Number} [options.notificationDuration=0] How long in seconds the final notification will live, or 0 for it to remain permanently. * @param {String} [options.notificationID='progressBar'] The id to give to the notification's DOM element. * * @example * function setup() { * createCanvas(100, 100); * * describe('A circle drawn in the middle of a gray square. The circle changes color from black to white, then repeats.'); * } * * function draw() { * background(200); * * // Style the circle. * let c = frameCount % 255; * fill(c); * * // Display the circle. * circle(50, 50, 25); * } * * // Save a 5-second gif when the user presses the 's' key. * function keyPressed() { * if (key === 's') { * saveGif('mySketch', 5); * } * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A circle drawn in the middle of a gray square. The circle changes color from black to white, then repeats.'); * } * * function draw() { * background(200); * * // Style the circle. * let c = frameCount % 255; * fill(c); * * // Display the circle. * circle(50, 50, 25); * } * * // Save a 5-second gif when the user presses the 's' key. * // Wait 1 second after the key press before recording. * function keyPressed() { * if (key === 's') { * saveGif('mySketch', 5, { delay: 1 }); * } * } */ fn.saveGif = async function( fileName, duration, options = { delay: 0, units: 'seconds', silent: false, notificationDuration: 0, notificationID: 'progressBar', reset: true } ) { // 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'); } // extract variables for more comfortable use const delay = (options && options.delay) || 0; // in seconds const units = (options && options.units) || 'seconds'; // either 'seconds' or 'frames' const silent = (options && options.silent) || false; const notificationDuration = (options && options.notificationDuration) || 0; const notificationID = (options && options.notificationID) || 'progressBar'; const resetAnimation = (options && options.reset !== undefined) ? options.reset : true; // if arguments in the options object are not correct, cancel operation if (typeof delay !== 'number') { throw TypeError('Delay parameter must be a number'); } // if units is not seconds nor frames, throw error if (units !== 'seconds' && units !== 'frames') { throw TypeError('Units parameter must be either "frames" or "seconds"'); } if (typeof silent !== 'boolean') { throw TypeError('Silent parameter must be a boolean'); } if (typeof notificationDuration !== 'number') { throw TypeError('Notification duration parameter must be a number'); } if (typeof notificationID !== 'string') { throw TypeError('Notification ID parameter must be a string'); } this._recording = true; // get the project's framerate 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 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, // 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; // 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 let frameIterator; let totalNumberOfFrames; if (resetAnimation) { frameIterator = nFramesDelay; this.frameCount = frameIterator; totalNumberOfFrames = nFrames + nFramesDelay; } else { frameIterator = this.frameCount + nFramesDelay; totalNumberOfFrames = frameIterator + nFrames; } const lastPixelDensity = this._renderer._pixelDensity; this.pixelDensity(1); // We first take every frame that we are going to use for the animation let frames = []; if (document.getElementById(notificationID) !== null) document.getElementById(notificationID).remove(); let p; if (!silent){ p = this.createP(''); p.id(notificationID); 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); } let pixels; let gl; if (this._renderer instanceof p5.RendererGL) { // if we have a WEBGL context, initialize the pixels array // and the gl context to use them inside the loop gl = this.drawingContext; pixels = new Uint8Array( gl.drawingBufferWidth * gl.drawingBufferHeight * 4 ); } // stop the loop since we are going to manually redraw this.noLoop(); // Defer execution until the rest of the call stack finishes, allowing the // rest of `setup` to be called (and, importantly, canvases hidden in setup // to be unhidden.) // // Waiting on this empty promise means we'll continue as soon as setup // finishes without waiting for another frame. await new Promise(requestAnimationFrame); 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(); await new Promise(requestAnimationFrame); // depending on the context we'll extract the pixels one way // or another let data = undefined; if (this._renderer instanceof p5.RendererGL) { 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, this.width, this.height); } else { data = this.drawingContext.getImageData(0, 0, this.width, this.height) .data; } frames.push(data); frameIterator++; if (!silent) { p.html( 'Saved frame ' + frames.length.toString() + ' out of ' + nFrames.toString() ); } await new Promise(resolve => setTimeout(resolve, 0)); } if (!silent) p.html('Frames processed, generating color palette...'); this.loop(); this.pixelDensity(lastPixelDensity); // create the gif encoder and the colorspace format const gif = ct(); // calculate the global palette for this set of frames const globalPalette = _generateGlobalPalette(frames); // Rather than using applyPalette() from the gifenc library, we use our // own function to map frame pixels to a palette color. This way, we can // cache palette color mappings between frames for extra performance, and // use our own caching mechanism to avoid flickering colors from cache // key collisions. const paletteCache = {}; const getIndexedFrame = frame => { const length = frame.length / 4; const index = new Uint8Array(length); for (let i = 0; i < length; i++) { const key = (frame[i * 4] << 24) | (frame[i * 4 + 1] << 16) | (frame[i * 4 + 2] << 8) | frame[i * 4 + 3]; if (paletteCache[key] === undefined) { paletteCache[key] = W( globalPalette, frame.slice(i * 4, (i + 1) * 4) ); } index[i] = paletteCache[key]; } return index; }; // 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 let prevIndexedFrame = []; for (let i = 0; i < frames.length; i++) { //const indexedFrame = applyPalette(frames[i], globalPaletteWithoutAlpha, 'rgba565'); const indexedFrame = getIndexedFrame(frames[i]); // Make a copy of the palette-applied frame before editing the original // to use transparent pixels const originalIndexedFrame = indexedFrame.slice(); if (i === 0) { gif.writeFrame(indexedFrame, this.width, this.height, { palette: globalPalette, delay: gifFrameDelay, dispose: 1 }); } else { // Matching pixels between frames can be set to full transparency, // allowing the previous frame's pixels to show through. We only do // this for pixels that get mapped to the same quantized color so that // the resulting image would be the same. for (let i = 0; i < indexedFrame.length; i++) { if (indexedFrame[i] === prevIndexedFrame[i]) { indexedFrame[i] = transparentIndex; } } // Write frame into the encoder gif.writeFrame(indexedFrame, this.width, this.height, { delay: gifFrameDelay, transparent: true, transparentIndex, dispose: 1 }); } prevIndexedFrame = originalIndexedFrame; if (!silent) { p.html( 'Rendered frame ' + i.toString() + ' out of ' + nFrames.toString() ); } // this just makes the process asynchronous, preventing // that the encoding locks up the browser await new Promise(resolve => setTimeout(resolve, 0)); } 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' }); frames = []; this._recording = false; this.loop(); if (!silent){ p.html('Done. Downloading your gif!🌸'); if(notificationDuration > 0) setTimeout(() => p.remove(), notificationDuration * 1000); } fn.downloadFile(blob, fileName, extension); }; function _flipPixels(pixels, width, height) { // 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 const halfHeight = parseInt(height / 2); const bytesPerRow = width * 4; // make a temp buffer to hold one row const temp = new Uint8Array(width * 4); for (let y = 0; y < halfHeight; ++y) { const topOffset = y * bytesPerRow; const 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) { // 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[f], f * frames[0].length); } // quantize this massive array into 256 colors and return it! let colorPalette = H(allColors, 256, { format: 'rgba4444', oneBitAlpha: true }); // 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; } /** * Helper function for loading GIF-based images */ async function _createGif(arrayBuffer, pImg) { // TODO: Replace with ImageDecoder once it is widely available // https://developer.mozilla.org/en-US/docs/Web/API/ImageDecoder const gifReader = new GifReader_1(arrayBuffer); pImg.width = pImg.canvas.width = gifReader.width; pImg.height = pImg.canvas.height = gifReader.height; const frames = []; const numFrames = gifReader.numFrames(); let framePixels = new Uint8ClampedArray(pImg.width * pImg.height * 4); const loadGIFFrameIntoImage = (frameNum, gifReader) => { try { gifReader.decodeAndBlitFrameRGBA(frameNum, framePixels); } catch (e) { p5._friendlyFileLoadError(8, pImg.src); throw e; } }; for (let j = 0; j < numFrames; j++) { const frameInfo = gifReader.frameInfo(j); const prevFrameData = pImg.drawingContext.getImageData( 0, 0, pImg.width, pImg.height ); framePixels = prevFrameData.data.slice(); loadGIFFrameIntoImage(j, gifReader); const imageData = new ImageData(framePixels, pImg.width, pImg.height); pImg.drawingContext.putImageData(imageData, 0, 0); let frameDelay = frameInfo.delay; // To maintain the default of 10FPS when frameInfo.delay equals to 0 if (frameDelay === 0) { frameDelay = 10; } frames.push({ image: pImg.drawingContext.getImageData(0, 0, pImg.width, pImg.height), delay: frameDelay * 10 //GIF stores delay in one-hundredth of a second, shift to ms }); // Some GIFs are encoded so that they expect the previous frame // to be under the current frame. This can occur at a sub-frame level // // Values : 0 - No disposal specified. The decoder is // not required to take any action. // 1 - Do not dispose. The graphic is to be left // in place. // 2 - Restore to background color. The area used by the // graphic must be restored to the background color. // 3 - Restore to previous. The decoder is required to // restore the area overwritten by the graphic with // what was there prior to rendering the graphic. // 4-7 - To be defined. if (frameInfo.disposal === 2) { // Restore background color pImg.drawingContext.clearRect( frameInfo.x, frameInfo.y, frameInfo.width, frameInfo.height ); } else if (frameInfo.disposal === 3) { // Restore previous pImg.drawingContext.putImageData( prevFrameData, 0, 0, frameInfo.x, frameInfo.y, frameInfo.width, frameInfo.height ); } } //Uses Netscape block encoding //to repeat forever, this will be 0 //to repeat just once, this will be null //to repeat N times (1 1) { pImg.gifProperties = { displayIndex: 0, loopLimit, loopCount: 0, frames, numFrames, playing: true, timeDisplayed: 0, lastChangeTime: 0 }; } return pImg; } /** * @private * @param {(LEFT|RIGHT|CENTER)} xAlign either LEFT, RIGHT or CENTER * @param {(TOP|BOTTOM|CENTER)} yAlign either TOP, BOTTOM or CENTER * @param {Number} dx * @param {Number} dy * @param {Number} dw * @param {Number} dh * @param {Number} sw * @param {Number} sh * @returns {Object} */ function _imageContain(xAlign, yAlign, dx, dy, dw, dh, sw, sh) { const r = Math.max(sw / dw, sh / dh); const [adjusted_dw, adjusted_dh] = [sw / r, sh / r]; let x = dx; let y = dy; if (xAlign === CENTER) { x += (dw - adjusted_dw) / 2; } else if (xAlign === RIGHT) { x += dw - adjusted_dw; } if (yAlign === CENTER) { y += (dh - adjusted_dh) / 2; } else if (yAlign === BOTTOM) { y += dh - adjusted_dh; } return { x, y, w: adjusted_dw, h: adjusted_dh }; } /** * @private * @param {(LEFT|RIGHT|CENTER)} xAlign either LEFT, RIGHT or CENTER * @param {(TOP|BOTTOM|CENTER)} yAlign either TOP, BOTTOM or CENTER * @param {Number} dw * @param {Number} dh * @param {Number} sx * @param {Number} sy * @param {Number} sw * @param {Number} sh * @returns {Object} */ function _imageCover(xAlign, yAlign, dw, dh, sx, sy, sw, sh) { const r = Math.max(dw / sw, dh / sh); const [adjusted_sw, adjusted_sh] = [dw / r, dh / r]; let x = sx; let y = sy; if (xAlign === CENTER) { x += (sw - adjusted_sw) / 2; } else if (xAlign === RIGHT) { x += sw - adjusted_sw; } if (yAlign === CENTER) { y += (sh - adjusted_sh) / 2; } else if (yAlign === BOTTOM) { y += sh - adjusted_sh; } return { x, y, w: adjusted_sw, h: adjusted_sh }; } /** * @private * @param {(CONTAIN|COVER)} [fit] either CONTAIN or COVER * @param {(LEFT|RIGHT|CENTER)} xAlign either LEFT, RIGHT or CENTER * @param {(TOP|BOTTOM|CENTER)} yAlign either TOP, BOTTOM or CENTER * @param {Number} dx * @param {Number} dy * @param {Number} dw * @param {Number} dh * @param {Number} sx * @param {Number} sy * @param {Number} sw * @param {Number} sh * @returns {Object} */ function _imageFit(fit, xAlign, yAlign, dx, dy, dw, dh, sx, sy, sw, sh) { if (fit === COVER) { const { x, y, w, h } = _imageCover( xAlign, yAlign, dw, dh, sx, sy, sw, sh ); sx = x; sy = y; sw = w; sh = h; } if (fit === CONTAIN) { const { x, y, w, h } = _imageContain( xAlign, yAlign, dx, dy, dw, dh, sw, sh ); dx = x; dy = y; dw = w; dh = h; } return { sx, sy, sw, sh, dx, dy, dw, dh }; } /** * Validates clipping params. Per drawImage spec sWidth and sHight cannot be * negative or greater than image intrinsic width and height * @private * @param {Number} sVal * @param {Number} iVal * @returns {Number} * @private */ function _sAssign(sVal, iVal) { if (sVal > 0 && sVal < iVal) { return sVal; } else { return iVal; } } /** * Draws an image to the canvas. * * The first parameter, `img`, is the source image to be drawn. `img` can be * any of the following objects: * - p5.Image * - p5.Element * - p5.Texture * - p5.Framebuffer * - p5.FramebufferTexture * * The second and third parameters, `dx` and `dy`, set the coordinates of the * destination image's top left corner. See * imageMode() for other ways to position images. * * ```js example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * background(50); * * // Draw the image. * image(img, 0, 0); * * describe('An image of the underside of a white umbrella with a gridded ceiling above.'); * } * ``` * * Here's a diagram that explains how optional parameters work in `image()`: * * * * The fourth and fifth parameters, `dw` and `dh`, are optional. They set the * the width and height to draw the destination image. By default, `image()` * draws the full source image at its original size. * * The sixth and seventh parameters, `sx` and `sy`, are also optional. * These coordinates define the top left corner of a subsection to draw from * the source image. * * The eighth and ninth parameters, `sw` and `sh`, are also optional. * They define the width and height of a subsection to draw from the source * image. By default, `image()` draws the full subsection that begins at * `(sx, sy)` and extends to the edges of the source image. * * The ninth parameter, `fit`, is also optional. It enables a subsection of * the source image to be drawn without affecting its aspect ratio. If * `CONTAIN` is passed, the full subsection will appear within the destination * rectangle. If `COVER` is passed, the subsection will completely cover the * destination rectangle. This may have the effect of zooming into the * subsection. * * The tenth and eleventh paremeters, `xAlign` and `yAlign`, are also * optional. They determine how to align the fitted subsection. `xAlign` can * be set to either `LEFT`, `RIGHT`, or `CENTER`. `yAlign` can be set to * either `TOP`, `BOTTOM`, or `CENTER`. By default, both `xAlign` and `yAlign` * are set to `CENTER`. * * @method image * @param {p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture|p5.Renderer|p5.Graphics} img image to display. * @param {Number} x x-coordinate of the top-left corner of the image. * @param {Number} y y-coordinate of the top-left corner of the image. * @param {Number} [width] width to draw the image. * @param {Number} [height] height to draw the image. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * background(50); * * // Draw the image. * image(img, 10, 10); * * describe('An image of the underside of a white umbrella with a gridded ceiling above. The image has dark gray borders on its left and top.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * background(50); * * // Draw the image 50x50. * image(img, 0, 0, 50, 50); * * describe('An image of the underside of a white umbrella with a gridded ceiling above. The image is drawn in the top left corner of a dark gray square.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * background(50); * * // Draw the center of the image. * image(img, 25, 25, 50, 50, 25, 25, 50, 50); * * describe('An image of a gridded ceiling drawn in the center of a dark gray square.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/moonwalk.jpg'); * createCanvas(100, 100); * * background(50); * * // Draw the image and scale it to fit within the canvas. * image(img, 0, 0, width, height, 0, 0, img.width, img.height, CONTAIN); * * describe('An image of an astronaut on the moon. The top and bottom borders of the image are dark gray.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense50.png'); * * createCanvas(100, 100); * * background(50); * * // Draw the image and scale it to cover the canvas. * image(img, 0, 0, width, height, 0, 0, img.width, img.height, COVER); * * describe('A pixelated image of the underside of a white umbrella with a gridded ceiling above.'); * } */ /** * @method image * @param {p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} img * @param {Number} dx the x-coordinate of the destination * rectangle in which to draw the source image * @param {Number} dy the y-coordinate of the destination * rectangle in which to draw the source image * @param {Number} dWidth the width of the destination rectangle * @param {Number} dHeight the height of the destination rectangle * @param {Number} sx the x-coordinate of the subsection of the source * image to draw into the destination rectangle * @param {Number} sy the y-coordinate of the subsection of the source * image to draw into the destination rectangle * @param {Number} [sWidth] the width of the subsection of the * source image to draw into the destination * rectangle * @param {Number} [sHeight] the height of the subsection of the * source image to draw into the destination rectangle * @param {(CONTAIN|COVER)} [fit] either CONTAIN or COVER * @param {(LEFT|RIGHT|CENTER)} [xAlign=CENTER] either LEFT, RIGHT or CENTER default is CENTER * @param {(TOP|BOTTOM|CENTER)} [yAlign=CENTER] either TOP, BOTTOM or CENTER default is CENTER */ fn.image = function( img, dx, dy, dWidth, dHeight, sx, sy, sWidth, sHeight, fit, xAlign, yAlign ) { // set defaults per spec: https://goo.gl/3ykfOq // p5._validateParameters('image', arguments); let defW = img.width; let defH = img.height; yAlign = yAlign || CENTER; xAlign = xAlign || CENTER; if (img.elt) { defW = defW !== undefined ? defW : img.elt.width; defH = defH !== undefined ? defH : img.elt.height; } if (img.elt && img.elt.videoWidth && !img.canvas) { // video no canvas defW = defW !== undefined ? defW : img.elt.videoWidth; defH = defH !== undefined ? defH : img.elt.videoHeight; } let _dx = dx; let _dy = dy; let _dw = dWidth || defW; let _dh = dHeight || defH; let _sx = sx || 0; let _sy = sy || 0; let _sw = sWidth !== undefined ? sWidth : defW; let _sh = sHeight !== undefined ? sHeight : defH; _sw = _sAssign(_sw, defW); _sh = _sAssign(_sh, defH); // This part needs cleanup and unit tests // see issues https://github.com/processing/p5.js/issues/1741 // and https://github.com/processing/p5.js/issues/1673 let pd = 1; if (img.elt && !img.canvas && img.elt.style.width) { //if img is video and img.elt.size() has been used and //no width passed to image() if (img.elt.videoWidth && !dWidth) { pd = img.elt.videoWidth; } else { //all other cases pd = img.elt.width; } pd /= parseInt(img.elt.style.width, 10); } _sx *= pd; _sy *= pd; _sh *= pd; _sw *= pd; let vals = canvas.modeAdjust( _dx, _dy, _dw, _dh, this._renderer.states.imageMode ); vals = _imageFit( fit, xAlign, yAlign, vals.x, vals.y, vals.w, vals.h, _sx, _sy, _sw, _sh ); // tint the image if there is a tint this._renderer.image( img, vals.sx, vals.sy, vals.sw, vals.sh, vals.dx, vals.dy, vals.dw, vals.dh ); }; /** * Tints images using a color. * * The version of `tint()` with one parameter interprets it one of four ways. * If the parameter is a number, it's interpreted as a grayscale value. If the * parameter is a string, it's interpreted as a CSS color string. An array of * `[R, G, B, A]` values or a p5.Color object can * also be used to set the tint color. * * The version of `tint()` with two parameters uses the first one as a * grayscale value and the second as an alpha value. For example, calling * `tint(255, 128)` will make an image 50% transparent. * * The version of `tint()` with three parameters interprets them as RGB or * HSB values, depending on the current * colorMode(). The optional fourth parameter * sets the alpha value. For example, `tint(255, 0, 0, 100)` will give images * a red tint and make them transparent. * * @method tint * @param {Number} v1 red or hue value. * @param {Number} v2 green or saturation value. * @param {Number} v3 blue or brightness. * @param {Number} [alpha] * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * // Left image. * image(img, 0, 0); * * // Right image. * // Tint with a CSS color string. * tint('red'); * image(img, 50, 0); * * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a red tint.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * // Left image. * image(img, 0, 0); * * // Right image. * // Tint with RGB values. * tint(255, 0, 0); * image(img, 50, 0); * * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a red tint.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * // Left. * image(img, 0, 0); * * // Right. * // Tint with RGBA values. * tint(255, 0, 0, 100); * image(img, 50, 0); * * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a transparent red tint.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * // Left. * image(img, 0, 0); * * // Right. * // Tint with grayscale and alpha values. * tint(255, 180); * image(img, 50, 0); * * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right is transparent.'); * } */ /** * @method tint * @param {String} value CSS color string. */ /** * @method tint * @param {Number} gray grayscale value. * @param {Number} [alpha] */ /** * @method tint * @param {Number[]} values array containing the red, green, blue & * alpha components of the color. */ /** * @method tint * @param {p5.Color} color the tint color */ fn.tint = function(...args) { // p5._validateParameters('tint', args); const c = this.color(...args); this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255])); }; /** * Removes the current tint set by tint(). * * `noTint()` restores images to their original colors. * * @method noTint * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100); * * // Left. * // Tint with a CSS color string. * tint('red'); * image(img, 0, 0); * * // Right. * // Remove the tint. * noTint(); * image(img, 50, 0); * * describe('Two images of an umbrella and a ceiling side-by-side. The image on the left has a red tint.'); * } */ fn.noTint = function() { this._renderer.states.setValue('tint', null); }; /** * Apply the current tint color to the input image, return the resulting * canvas. * * @private * @param {p5.Image} The image to be tinted * @return {canvas} The resulting tinted canvas */ // fn._getTintedImageCanvas = // p5.Renderer2D.prototype._getTintedImageCanvas; /** * Changes the location from which images are drawn when * image() is called. * * By default, the first * two parameters of image() are the x- and * y-coordinates of the image's upper-left corner. The next parameters are * its width and height. This is the same as calling `imageMode(CORNER)`. * * `imageMode(CORNERS)` also uses the first two parameters of * image() as the x- and y-coordinates of the image's * top-left corner. The third and fourth parameters are the coordinates of its * bottom-right corner. * * `imageMode(CENTER)` uses the first two parameters of * image() as the x- and y-coordinates of the image's * center. The next parameters are its width and height. * * @method imageMode * @param {(CORNER|CORNERS|CENTER)} mode either CORNER, CORNERS, or CENTER. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * background(200); * * // Use CORNER mode. * imageMode(CORNER); * * // Display the image. * image(img, 10, 10, 50, 50); * * describe('A square image of a brick wall is drawn at the top left of a gray square.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * background(200); * * // Use CORNERS mode. * imageMode(CORNERS); * * // Display the image. * image(img, 10, 10, 90, 40); * * describe('An image of a brick wall is drawn on a gray square. The image is squeezed into a small rectangular area.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * background(200); * * // Use CENTER mode. * imageMode(CENTER); * * // Display the image. * image(img, 50, 50, 80, 80); * * describe('A square image of a brick wall is drawn on a gray square.'); * } */ fn.imageMode = function(m) { // p5._validateParameters('imageMode', arguments); if ( m === CORNER || m === CORNERS || m === CENTER ) { this._renderer.states.setValue('imageMode', m); } }; } if(typeof p5 !== 'undefined'){ loadingDisplaying(p5, p5.prototype); } /** * @module Image * @submodule Pixels * @for p5 * @requires core */ function pixels(p5, fn){ /** * Copies a region of pixels from one image to another. * * The first parameter, `srcImage`, is the * p5.Image object to blend. * * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region * to blend from the source image. `(sx, sy)` is the top-left corner of the * region. `sw` and `sh` are the regions width and height. * * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region * of the canvas to blend into. `(dx, dy)` is the top-left corner of the * region. `dw` and `dh` are the regions width and height. * * The tenth parameter, `blendMode`, sets the effect used to blend the images' * colors. The options are `BLEND`, `DARKEST`, `LIGHTEST`, `DIFFERENCE`, * `MULTIPLY`, `EXCLUSION`, `SCREEN`, `REPLACE`, `OVERLAY`, `HARD_LIGHT`, * `SOFT_LIGHT`, `DODGE`, `BURN`, `ADD`, or `NORMAL` * * @method blend * @param {p5.Image} srcImage source image. * @param {Integer} sx x-coordinate of the source's upper-left corner. * @param {Integer} sy y-coordinate of the source's upper-left corner. * @param {Integer} sw source image width. * @param {Integer} sh source image height. * @param {Integer} dx x-coordinate of the destination's upper-left corner. * @param {Integer} dy y-coordinate of the destination's upper-left corner. * @param {Integer} dw destination image width. * @param {Integer} dh destination image height. * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode the blend mode. either * BLEND, DARKEST, LIGHTEST, DIFFERENCE, * MULTIPLY, EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT, * SOFT_LIGHT, DODGE, BURN, ADD or NORMAL. * * @example * let img0; * let img1; * * async function setup() { * // Load the images. * img0 = await loadImage('assets/rockies.jpg'); * img1 = await loadImage('assets/bricks_third.jpg'); * * createCanvas(100, 100); * * // Use the mountains as the background. * background(img0); * * // Display the bricks. * image(img1, 0, 0); * * // Display the bricks faded into the landscape. * blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, LIGHTEST); * * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears faded on the right of the image.'); * } * * @example * let img0; * let img1; * * async function setup() { * // Load the images. * img0 = await loadImage('assets/rockies.jpg'); * img1 = await loadImage('assets/bricks_third.jpg'); * * createCanvas(100, 100); * * // Use the mountains as the background. * background(img0); * * // Display the bricks. * image(img1, 0, 0); * * // Display the bricks partially transparent. * blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, DARKEST); * * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears transparent on the right of the image.'); * } * * @example * let img0; * let img1; * * async function setup() { * // Load the images. * img0 = await loadImage('assets/rockies.jpg'); * img1 = await loadImage('assets/bricks_third.jpg'); * * createCanvas(100, 100); * * // Use the mountains as the background. * background(img0); * * // Display the bricks. * image(img1, 0, 0); * * // Display the bricks washed out into the landscape. * blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, ADD); * * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears washed out on the right of the image.'); * } */ /** * @method blend * @param {Integer} sx * @param {Integer} sy * @param {Integer} sw * @param {Integer} sh * @param {Integer} dx * @param {Integer} dy * @param {Integer} dw * @param {Integer} dh * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode */ fn.blend = function(...args) { // p5._validateParameters('blend', args); if (this._renderer) { this._renderer.blend(...args); } else { p5.Renderer2D.prototype.blend.apply(this, args); } }; /** * Copies pixels from a source image to a region of the canvas. * * The first parameter, `srcImage`, is the * p5.Image object to blend. The source image can be * the canvas itself or a * p5.Image object. `copy()` will scale pixels from * the source region if it isn't the same size as the destination region. * * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region * to copy from the source image. `(sx, sy)` is the top-left corner of the * region. `sw` and `sh` are the region's width and height. * * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region * of the canvas to copy into. `(dx, dy)` is the top-left corner of the * region. `dw` and `dh` are the region's width and height. * * @method copy * @param {p5.Image|p5.Element} srcImage source image. * @param {Integer} sx x-coordinate of the source's upper-left corner. * @param {Integer} sy y-coordinate of the source's upper-left corner. * @param {Integer} sw source image width. * @param {Integer} sh source image height. * @param {Integer} dx x-coordinate of the destination's upper-left corner. * @param {Integer} dy y-coordinate of the destination's upper-left corner. * @param {Integer} dw destination image width. * @param {Integer} dh destination image height. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Use the mountains as the background. * background(img); * * // Copy a region of pixels to another spot. * copy(img, 7, 22, 10, 10, 35, 25, 50, 50); * * // Outline the copied region. * stroke(255); * noFill(); * square(7, 22, 10); * * describe('An image of a mountain landscape. A square region is outlined in white. A larger square contains a pixelated view of the outlined region.'); * } */ /** * @method copy * @param {Integer} sx * @param {Integer} sy * @param {Integer} sw * @param {Integer} sh * @param {Integer} dx * @param {Integer} dy * @param {Integer} dw * @param {Integer} dh */ fn.copy = function(...args) { let srcImage, sx, sy, sw, sh, dx, dy, dw, dh; if (args.length === 9) { srcImage = args[0]; sx = args[1]; sy = args[2]; sw = args[3]; sh = args[4]; dx = args[5]; dy = args[6]; dw = args[7]; dh = args[8]; } else if (args.length === 8) { srcImage = this; sx = args[0]; sy = args[1]; sw = args[2]; sh = args[3]; dx = args[4]; dy = args[5]; dw = args[6]; dh = args[7]; } else { throw new Error('Signature not supported'); } fn._copyHelper(this, srcImage, sx, sy, sw, sh, dx, dy, dw, dh); }; fn._copyHelper = ( dstImage, srcImage, sx, sy, sw, sh, dx, dy, dw, dh ) => { const s = srcImage.canvas.width / srcImage.width; // adjust coord system for 3D when renderer // ie top-left = -width/2, -height/2 let sxMod = 0; let syMod = 0; if (srcImage._renderer && srcImage._renderer.isP3D) { sxMod = srcImage.width / 2; syMod = srcImage.height / 2; } if (dstImage._renderer && dstImage._renderer.isP3D) { dstImage.push(); dstImage.resetMatrix(); dstImage.noLights(); dstImage.blendMode(dstImage.BLEND); dstImage.imageMode(dstImage.CORNER); dstImage._renderer.image( srcImage, sx + sxMod, sy + syMod, sw, sh, dx, dy, dw, dh ); dstImage.pop(); } else { dstImage.drawingContext.drawImage( srcImage.canvas, s * (sx + sxMod), s * (sy + syMod), s * sw, s * sh, dx, dy, dw, dh ); } }; /** * Applies an image filter to the canvas. * * The preset options are: * * `INVERT` * Inverts the colors in the image. No parameter is used. * * `GRAY` * Converts the image to grayscale. No parameter is used. * * `THRESHOLD` * Converts the image to black and white. Pixels with a grayscale value * above a given threshold are converted to white. The rest are converted to * black. The threshold must be between 0.0 (black) and 1.0 (white). If no * value is specified, 0.5 is used. * * `OPAQUE` * Sets the alpha channel to entirely opaque. No parameter is used. * * `POSTERIZE` * Limits the number of colors in the image. Each color channel is limited to * the number of colors specified. Values between 2 and 255 are valid, but * results are most noticeable with lower values. The default value is 4. * * `BLUR` * Blurs the image. The level of blurring is specified by a blur radius. Larger * values increase the blur. The default value is 4. A gaussian blur is used * in `P2D` mode. A box blur is used in `WEBGL` mode. * * `ERODE` * Reduces the light areas. No parameter is used. * * `DILATE` * Increases the light areas. No parameter is used. * * `filter()` uses WebGL in the background by default because it's faster. * This can be disabled in `P2D` mode by adding a `false` argument, as in * `filter(BLUR, false)`. This may be useful to keep computation off the GPU * or to work around a lack of WebGL support. * * In WebgL mode, `filter()` can also use custom shaders. See * createFilterShader() for more * information. * * * @method filter * @param {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR)} filterType either THRESHOLD, GRAY, OPAQUE, INVERT, * POSTERIZE, BLUR, ERODE, DILATE or BLUR. * @param {Number} [filterParam] parameter unique to each filter. * @param {Boolean} [useWebGL=true] flag to control whether to use fast * WebGL filters (GPU) or original image * filters (CPU); defaults to `true`. * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the INVERT filter. * filter(INVERT); * * describe('A blue brick wall.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the GRAY filter. * filter(GRAY); * * describe('A brick wall drawn in grayscale.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the THRESHOLD filter. * filter(THRESHOLD); * * describe('A brick wall drawn in black and white.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the OPAQUE filter. * filter(OPAQUE); * * describe('A red brick wall.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the POSTERIZE filter. * filter(POSTERIZE, 3); * * describe('An image of a red brick wall drawn with limited color palette.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the BLUR filter. * filter(BLUR, 3); * * describe('A blurry image of a red brick wall.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the DILATE filter. * filter(DILATE); * * describe('A red brick wall with bright lines between each brick.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the ERODE filter. * filter(ERODE); * * describe('A red brick wall with faint lines between each brick.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Apply the BLUR filter. * // Don't use WebGL. * filter(BLUR, 3, false); * * describe('A blurry image of a red brick wall.'); * } */ /** * @method getFilterGraphicsLayer * @private * @returns {p5.Graphics} */ fn.getFilterGraphicsLayer = function() { return this._renderer.getFilterGraphicsLayer(); }; /** * @method filter * @param {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR)} filterType * @param {Number} [filterParam] * @param {Boolean} [useWebGL=true] */ /** * @method filter * @param {p5.Shader} shaderFilter shader that's been loaded, with the * frag shader using a `tex0` uniform. */ fn.filter = function(...args) { // p5._validateParameters('filter', args); let { shader, operation, value, useWebGL } = parseFilterArgs(...args); // when passed a shader, use it directly if (this._renderer.isP3D && shader) { this._renderer.filter(shader); return; } // when opting out of webgl, use old pixels method if (!useWebGL && !this._renderer.isP3D) { if (this.canvas !== undefined) { Filters.apply(this.canvas, Filters[operation], value); } else { Filters.apply(this.elt, Filters[operation], value); } return; } if(!useWebGL && this._renderer.isP3D) { console.warn('filter() with useWebGL=false is not supported in WEBGL'); } // when this is a webgl renderer, apply constant shader filter if (this._renderer.isP3D) { this._renderer.filter(operation, value); } // when this is P2D renderer, create/use hidden webgl renderer else { if (shader) { this._renderer.filterRenderer.setOperation(operation, value, shader); } else { this._renderer.filterRenderer.setOperation(operation, value); } this._renderer.filterRenderer.applyFilter(); } }; function parseFilterArgs(...args) { // args could be: // - operation, value, [useWebGL] // - operation, [useWebGL] // - shader let result = { shader: undefined, operation: undefined, value: undefined, useWebGL: true }; if (args[0] instanceof p5.Shader) { result.shader = args[0]; return result; } else { result.operation = args[0]; } if (args.length > 1 && typeof args[1] === 'number') { result.value = args[1]; } if (args[args.length-1] === false) { result.useWebGL = false; } return result; } /** * Gets a pixel or a region of pixels from the canvas. * * `get()` is easy to use but it's not as fast as * pixels. Use pixels * to read many pixel values. * * The version of `get()` with no parameters returns the entire canvas. * * The version of `get()` with two parameters interprets them as * coordinates. It returns an array with the `[R, G, B, A]` values of the * pixel at the given point. * * The version of `get()` with four parameters interprets them as coordinates * and dimensions. It returns a subsection of the canvas as a * p5.Image object. The first two parameters are the * coordinates for the upper-left corner of the subsection. The last two * parameters are the width and height of the subsection. * * Use p5.Image.get() to work directly with * p5.Image objects. * * @method get * @param {Number} x x-coordinate of the pixel. * @param {Number} y y-coordinate of the pixel. * @param {Number} w width of the subsection to be returned. * @param {Number} h height of the subsection to be returned. * @return {p5.Image} subsection as a p5.Image object. * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Get the entire canvas. * let c = get(); * * // Display half the canvas. * image(c, 50, 0); * * describe('Two identical mountain landscapes shown side-by-side.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Get the color of a pixel. * let c = get(50, 90); * * // Style the square with the pixel's color. * fill(c); * noStroke(); * * // Display the square. * square(25, 25, 50); * * describe('A mountain landscape with an olive green square in its center.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0); * * // Get a region of the image. * let c = get(0, 0, 50, 50); * * // Display the region. * image(c, 50, 50); * * describe('A mountain landscape drawn on top of another mountain landscape.'); * } */ /** * @method get * @return {p5.Image} whole canvas as a p5.Image. */ /** * @method get * @param {Number} x * @param {Number} y * @return {Number[]} color of the pixel at (x, y) in array format `[R, G, B, A]`. */ fn.get = function(x, y, w, h) { // p5._validateParameters('get', arguments); return this._renderer.get(...arguments); }; /** * Loads the current value of each pixel on the canvas into the * pixels array. * * `loadPixels()` must be called before reading from or writing to * pixels. * * @method loadPixels * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0, 100, 100); * * // Get the pixel density. * let d = pixelDensity(); * * // Calculate the halfway index in the pixels array. * let halfImage = 4 * (d * width) * (d * height / 2); * * // Load the pixels array. * loadPixels(); * * // Copy the top half of the canvas to the bottom. * for (let i = 0; i < halfImage; i += 1) { * pixels[i + halfImage] = pixels[i]; * } * * // Update the canvas. * updatePixels(); * * describe('Two identical images of mountain landscapes, one on top of the other.'); * } */ fn.loadPixels = function(...args) { // p5._validateParameters('loadPixels', args); return this._renderer.loadPixels(); }; /** * Sets the color of a pixel or draws an image to the canvas. * * `set()` is easy to use but it's not as fast as * pixels. Use pixels * to set many pixel values. * * `set()` interprets the first two parameters as x- and y-coordinates. It * interprets the last parameter as a grayscale value, a `[R, G, B, A]` pixel * array, a p5.Color object, or a * p5.Image object. If an image is passed, the first * two parameters set the coordinates for the image's upper-left corner, * regardless of the current imageMode(). * * updatePixels() must be called after using * `set()` for changes to appear. * * @method set * @param {Number} x x-coordinate of the pixel. * @param {Number} y y-coordinate of the pixel. * @param {Number|Number[]|Object} c grayscale value | pixel array | * p5.Color object | p5.Image to copy. * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Set four pixels to black. * set(30, 20, 0); * set(85, 20, 0); * set(85, 75, 0); * set(30, 75, 0); * * // Update the canvas. * updatePixels(); * * describe('Four black dots arranged in a square drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.Color object. * let black = color(0); * * // Set four pixels to black. * set(30, 20, black); * set(85, 20, black); * set(85, 75, black); * set(30, 75, black); * * // Update the canvas. * updatePixels(); * * describe('Four black dots arranged in a square drawn on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(255); * * // Draw a horizontal color gradient. * for (let x = 0; x < 100; x += 1) { * for (let y = 0; y < 100; y += 1) { * // Calculate the grayscale value. * let c = map(x, 0, 100, 0, 255); * * // Set the pixel using the grayscale value. * set(x, y, c); * } * } * * // Update the canvas. * updatePixels(); * * describe('A horiztonal color gradient from black to white.'); * } * * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Use the image to set all pixels. * set(0, 0, img); * * // Update the canvas. * updatePixels(); * * describe('An image of a mountain landscape.'); * } */ fn.set = function(x, y, imgOrCol) { this._renderer.set(x, y, imgOrCol); }; /** * Updates the canvas with the RGBA values in the * pixels array. * * `updatePixels()` only needs to be called after changing values in the * pixels array. Such changes can be made directly * after calling loadPixels() or by calling * set(). * * @method updatePixels * @param {Number} [x] x-coordinate of the upper-left corner of region * to update. * @param {Number} [y] y-coordinate of the upper-left corner of region * to update. * @param {Number} [w] width of region to update. * @param {Number} [h] height of region to update. * @example * let img; * * async function setup() { * // Load the image. * img = await loadImage('assets/rockies.jpg'); * * createCanvas(100, 100); * * // Display the image. * image(img, 0, 0, 100, 100); * * // Get the pixel density. * let d = pixelDensity(); * * // Calculate the halfway index in the pixels array. * let halfImage = 4 * (d * width) * (d * height / 2); * * // Load the pixels array. * loadPixels(); * * // Copy the top half of the canvas to the bottom. * for (let i = 0; i < halfImage; i += 1) { * pixels[i + halfImage] = pixels[i]; * } * * // Update the canvas. * updatePixels(); * * describe('Two identical images of mountain landscapes, one on top of the other.'); * } */ fn.updatePixels = function(x, y, w, h) { // p5._validateParameters('updatePixels', arguments); // graceful fail - if loadPixels() or set() has not been called, pixel // array will be empty, ignore call to updatePixels() if (this.pixels.length === 0) { return; } this._renderer.updatePixels(x, y, w, h); }; /** * An array containing the color of each pixel on the canvas. * * Colors are stored as numbers representing red, green, blue, and alpha * (RGBA) values. `pixels` is a one-dimensional array for performance reasons. * * Each pixel occupies four elements in the `pixels` array, one for each RGBA * value. For example, the pixel at coordinates (0, 0) stores its RGBA values * at `pixels[0]`, `pixels[1]`, `pixels[2]`, and `pixels[3]`, respectively. * The next pixel at coordinates (1, 0) stores its RGBA values at `pixels[4]`, * `pixels[5]`, `pixels[6]`, and `pixels[7]`. And so on. The `pixels` array * for a 100×100 canvas has 100 × 100 × 4 = 40,000 elements. * * Some displays use several smaller pixels to set the color at a single * point. The pixelDensity() function returns * the pixel density of the canvas. High density displays often have a * pixelDensity() of 2. On such a display, the * `pixels` array for a 100×100 canvas has 200 × 200 × 4 = * 160,000 elements. * * Accessing the RGBA values for a point on the canvas requires a little math * as shown below. The loadPixels() function * must be called before accessing the `pixels` array. The * updatePixels() function must be called * after any changes are made. * * @property {Number[]} pixels * * @example * function setup() { * createCanvas(100, 100); * background(128); * * // Load the pixels array. * loadPixels(); * * // Set the dot's coordinates. * let x = 50; * let y = 50; * * // Get the pixel density. * let d = pixelDensity(); * * // Set the pixel(s) at the center of the canvas black. * for (let i = 0; i < d; i += 1) { * for (let j = 0; j < d; j += 1) { * let index = 4 * ((y * d + j) * width * d + (x * d + i)); * // Red. * pixels[index] = 0; * // Green. * pixels[index + 1] = 0; * // Blue. * pixels[index + 2] = 0; * // Alpha. * pixels[index + 3] = 255; * } * } * * // Update the canvas. * updatePixels(); * * describe('A black dot in the middle of a gray rectangle.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Load the pixels array. * loadPixels(); * * // Get the pixel density. * let d = pixelDensity(); * * // Calculate the halfway index in the pixels array. * let halfImage = 4 * (d * width) * (d * height / 2); * * // Make the top half of the canvas red. * for (let i = 0; i < halfImage; i += 4) { * // Red. * pixels[i] = 255; * // Green. * pixels[i + 1] = 0; * // Blue. * pixels[i + 2] = 0; * // Alpha. * pixels[i + 3] = 255; * } * * // Update the canvas. * updatePixels(); * * describe('A red rectangle drawn above a gray rectangle.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Create a p5.Color object. * let pink = color(255, 102, 204); * * // Load the pixels array. * loadPixels(); * * // Get the pixel density. * let d = pixelDensity(); * * // Calculate the halfway index in the pixels array. * let halfImage = 4 * (d * width) * (d * height / 2); * * // Make the top half of the canvas red. * for (let i = 0; i < halfImage; i += 4) { * pixels[i] = red(pink); * pixels[i + 1] = green(pink); * pixels[i + 2] = blue(pink); * pixels[i + 3] = alpha(pink); * } * * // Update the canvas. * updatePixels(); * * describe('A pink rectangle drawn above a gray rectangle.'); * } */ } if(typeof p5 !== 'undefined'){ pixels(p5, p5.prototype); } class MatrixInterface { // Private field to store the matrix #matrix = null; constructor(...args) { if (this.constructor === MatrixInterface) { throw new Error("Class is of abstract type and can't be instantiated"); } // TODO: don't check this at runtime but still at compile time /*const methods = [ 'add', 'setElement', 'reset', 'set', 'get', 'copy', 'clone', 'diagonal', 'row', 'column', 'transpose', 'mult', 'multiplyVec', 'invert', 'createSubMatrix3x3', 'inverseTranspose4x4', 'apply', 'scale', 'rotate4x4', 'translate', 'rotateX', 'rotateY', 'rotateZ', 'perspective', 'ortho', 'multiplyVec4', 'multiplyPoint', 'multiplyAndNormalizePoint', 'multiplyDirection', 'multiplyVec3' ]; methods.forEach(method => { if (this[method] === undefined) { throw new Error(`${method}() method must be implemented`); } });*/ } } /** * @module Math */ const isPerfectSquare = arr => { const sqDimention = Math.sqrt(arr.length); if (sqDimention % 1 !== 0) { throw new Error('Array length must be a perfect square.'); } return true; }; let GLMAT_ARRAY_TYPE = Array; let isMatrixArray = x => Array.isArray(x); if (typeof Float32Array !== 'undefined') { GLMAT_ARRAY_TYPE = Float32Array; isMatrixArray = x => Array.isArray(x) || x instanceof Float32Array; } class Matrix extends MatrixInterface { matrix; #sqDimention; constructor(...args) { super(...args); // This is default behavior when object // instantiated using createMatrix() if (isMatrixArray(args[0]) && isPerfectSquare(args[0])) { const sqDimention = Math.sqrt(args[0].length); this.#sqDimention = sqDimention; this.matrix = GLMAT_ARRAY_TYPE.from(args[0]); } else if (typeof args[0] === 'number') { this.#sqDimention = Number(args[0]); this.matrix = this.#createIdentityMatrix(args[0]); } return this; } /** * Returns the 3x3 matrix if the dimensions are 3x3, otherwise returns `undefined`. * * This method returns the matrix if its dimensions are 3x3. * If the matrix is not 3x3, it returns `undefined`. * * @returns {Array|undefined} The 3x3 matrix or `undefined` if the matrix is not 3x3. */ get mat3() { if (this.#sqDimention === 3) { return this.matrix; } else { return undefined; } } /** * Returns the 4x4 matrix if the dimensions are 4x4, otherwise returns `undefined`. * * This method returns the matrix if its dimensions are 4x4. * If the matrix is not 4x4, it returns `undefined`. * * @returns {Array|undefined} The 4x4 matrix or `undefined` if the matrix is not 4x4. */ get mat4() { if (this.#sqDimention === 4) { return this.matrix; } else { return undefined; } } /** * Adds the corresponding elements of the given matrix to this matrix, if the dimentions are the same. * * @param {Matrix} matrix - The matrix to add to this matrix. It must have the same dimensions as this matrix. * @returns {Matrix} The resulting matrix after addition. * @throws {Error} If the matrices do not have the same dimensions. * * @example * // META:norender * const matrix1 = new p5.Matrix([1, 2, 3]); * const matrix2 = new p5.Matrix([4, 5, 6]); * matrix1.add(matrix2); // matrix1 is now [5, 7, 9] * * @example * // META:norender * function setup() { * const matrix1 = new p5.Matrix([1, 2, 3, 4]); * const matrix2 = new p5.Matrix([5, 6, 7, 8]); * matrix1.add(matrix2); * console.log(matrix1.matrix); // Output: [6, 8, 10, 12] * } */ add(matrix) { if (this.matrix.length !== matrix.matrix.length) { throw new Error('Matrices must be of the same dimension to add.'); } for (let i = 0; i < this.matrix.length; i++) { this.matrix[i] += matrix.matrix[i]; } return this; } /** * Sets the value of a specific element in the matrix in column-major order. * * A matrix is stored in column-major order, meaning elements are arranged column by column. * This function allows you to update or change the value of a specific element * in the matrix by specifying its index in the column-major order and the new value. * * Parameters: * - `index` (number): The position in the matrix where the value should be set. * Indices start from 0 and follow column-major order. * - `value` (any): The new value you want to assign to the specified element. * * Example: * If you have the following 3x3 matrix stored in column-major order: * ``` * [ * 1, 4, 7, // Column 1 * 2, 5, 8, // Column 2 * 3, 6, 9 // Column 3 * ] * ``` * Calling `setElement(4, 10)` will update the element at index 4 * (which corresponds to row 2, column 2 in row-major order) to `10`. * The updated matrix will look like this: * ``` * [ * 1, 4, 7, * 2, 10, 8, * 3, 6, 9 * ] * ``` * * This function is useful for modifying specific parts of the matrix without * having to recreate the entire structure. * * @param {Number} index - The position in the matrix where the value should be set. * Must be a non-negative integer less than the length of the matrix. * @param {Number} value - The new value to be assigned to the specified position in the matrix. * @returns {Matrix} The current instance of the Matrix, allowing for method chaining. * * @example * // META:norender * // Assuming matrix is an instance of Matrix with initial values [1, 2, 3, 4] matrix.setElement(2, 99); * // Now the matrix values are [1, 2, 99, 4] * function setup() { * const matrix = new p5.Matrix([1, 2, 3, 4]); * matrix.setElement(2, 99); * console.log(matrix.matrix); // Output: [1, 2, 99, 4] * } */ setElement(index, value) { if (index >= 0 && index < this.matrix.length) { this.matrix[index] = value; } return this; } /** * Resets the current matrix to an identity matrix. * * This method replaces the current matrix with an identity matrix of the same dimensions. * An identity matrix is a square matrix with ones on the main diagonal and zeros elsewhere. * This is useful for resetting transformations or starting fresh with a clean matrix. * * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining. * * @example * // META:norender * // Resetting a 4x4 matrix to an identity matrix * const matrix = new p5.Matrix(4); * matrix.scale(2, 2, 2); // Apply some transformations * console.log(matrix.matrix); // Output: Transformed matrix * matrix.reset(); // Reset to identity matrix * console.log(matrix.matrix); // Output: Identity matrix * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); * matrix.scale(2, 2, 2); // Apply scaling transformation * console.log("Before reset:", matrix.matrix); * matrix.reset(); // Reset to identity matrix * console.log("After reset:", matrix.matrix); * } */ reset() { this.matrix = this.#createIdentityMatrix(this.#sqDimention); return this; } /** * Replace the entire contents of a NxN matrix. * * This method allows you to replace the values of the current matrix with * those from another matrix, an array, or individual arguments. The input * can be a `Matrix` instance, an array of numbers, or individual numbers * that match the dimensions of the current matrix. The values are copied * without referencing the source object, ensuring that the original input * remains unchanged. * * If the input dimensions do not match the current matrix, an error will * be thrown to ensure consistency. * * @param {Matrix|Float32Array|Number[]} [inMatrix] - The input matrix, array, * or individual numbers to replace the current matrix values. * @returns {Matrix} The current instance of the Matrix class, allowing for * method chaining. * * @example * // META:norender * // Replacing the contents of a matrix with another matrix * const matrix1 = new p5.Matrix([1, 2, 3, 4]); * const matrix2 = new p5.Matrix([5, 6, 7, 8]); * matrix1.set(matrix2); * console.log(matrix1.matrix); // Output: [5, 6, 7, 8] * * // Replacing the contents of a matrix with an array * const matrix = new p5.Matrix([1, 2, 3, 4]); * matrix.set([9, 10, 11, 12]); * console.log(matrix.matrix); // Output: [9, 10, 11, 12] * * // Replacing the contents of a matrix with individual numbers * const matrix = new p5.Matrix(4); // Creates a 4x4 identity matrix * matrix.set(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); * console.log(matrix.matrix); // Output: [1, 2, 3, ..., 16] * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix([1, 2, 3, 4]); * console.log("Before set:", matrix.matrix); * matrix.set([5, 6, 7, 8]); * console.log("After set:", matrix.matrix); // Output: [5, 6, 7, 8] * } */ set(inMatrix) { let refArray = GLMAT_ARRAY_TYPE.from([...arguments]); if (inMatrix instanceof Matrix) { refArray = GLMAT_ARRAY_TYPE.from(inMatrix.matrix); } else if (isMatrixArray(inMatrix)) { refArray = GLMAT_ARRAY_TYPE.from(inMatrix); } if (refArray.length !== this.matrix.length) { p5._friendlyError( `Expected same dimensions values but received different ${refArray.length}.`, 'p5.Matrix.set' ); return this; } this.matrix = refArray; return this; } /** * Gets a copy of the matrix, returns a p5.Matrix object. * * This method creates a new instance of the `Matrix` class and copies the * current matrix values into it. The returned matrix is independent of the * original, meaning changes to the copy will not affect the original matrix. * * This is useful when you need to preserve the current state of a matrix * while performing operations on a duplicate. * * @return {p5.Matrix} A new instance of the `Matrix` class containing the * same values as the original matrix. * * @example * // META:norender * function setup() { * const originalMatrix = new p5.Matrix([1, 2, 3, 4]); * const copiedMatrix = originalMatrix.get(); * console.log("Original Matrix:", originalMatrix.matrix); // Output: [1, 2, 3, 4] * console.log("Copied Matrix:", copiedMatrix.matrix); // Output: [1, 2, 3, 4] * * // Modify the copied matrix * copiedMatrix.setElement(2, 99); * console.log("Modified Copied Matrix:", copiedMatrix.matrix); // Output: [1, 2, 99, 4] * console.log("Original Matrix remains unchanged:", originalMatrix.matrix); // Output: [1, 2, 3, 4] * } */ get() { return new Matrix(this.matrix); // TODO: Pass p5 } /** * Return a copy of this matrix. * If this matrix is 4x4, a 4x4 matrix with exactly the same entries will be * generated. The same is true if this matrix is 3x3 or any NxN matrix. * * This method is useful when you need to preserve the current state of a matrix * while performing operations on a duplicate. The returned matrix is independent * of the original, meaning changes to the copy will not affect the original matrix. * * @return {p5.Matrix} The result matrix. * * @example * // META:norender * function setup() { * const originalMatrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const copiedMatrix = originalMatrix.copy(); * console.log("Original Matrix:", originalMatrix.matrix); * console.log("Copied Matrix:", copiedMatrix.matrix); * * // Modify the copied matrix * copiedMatrix.setElement(4, 99); * console.log("Modified Copied Matrix:", copiedMatrix.matrix); * console.log("Original Matrix remains unchanged:", originalMatrix.matrix); * } */ copy() { return new Matrix(this.matrix); } /** * Creates a copy of the current matrix instance. * This method is useful when you need a duplicate of the matrix * without modifying the original one. * * @returns {Matrix} A new matrix instance that is a copy of the current matrix. * * @example * // META:norender * function setup() { * const originalMatrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const clonedMatrix = originalMatrix.clone(); * console.log("Original Matrix:", originalMatrix.matrix); * console.log("Cloned Matrix:", clonedMatrix.matrix); * * // Modify the cloned matrix * clonedMatrix.setElement(4, 99); * console.log("Modified Cloned Matrix:", clonedMatrix.matrix); * console.log("Original Matrix remains unchanged:", originalMatrix.matrix); * } */ clone() { return this.copy(); } /** * Returns the diagonal elements of the matrix in the form of an array. * A NxN matrix will return an array of length N. * * This method extracts the diagonal elements of the matrix, which are the * elements where the row index equals the column index. For example, in a * 3x3 matrix: * ``` * [ * 1, 2, 3, * 4, 5, 6, * 7, 8, 9 * ] * ``` * The diagonal elements are [1, 5, 9]. * * This is useful for operations that require the main diagonal of a matrix, * such as calculating the trace of a matrix or verifying if a matrix is diagonal. * * @return {Number[]} An array obtained by arranging the diagonal elements * of the matrix in ascending order of index. * * @example * // META:norender * // Extracting the diagonal elements of a matrix * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const diagonal = matrix.diagonal(); // [1, 5, 9] * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const diagonal = matrix.diagonal(); * console.log("Diagonal elements:", diagonal); // Output: [1, 5, 9] * } */ diagonal() { const diagonal = []; for (let i = 0; i < this.#sqDimention; i++) { diagonal.push(this.matrix[i * (this.#sqDimention + 1)]); } return diagonal; } /** * This function is only for 3x3 matrices A function that returns a row vector of a NxN matrix. * * This method extracts a specific row from the matrix and returns it as a `p5.Vector`. * The row is determined by the `columnIndex` parameter, which specifies the column * index of the matrix. This is useful for operations that require working with * individual rows of a matrix, such as row transformations or dot products. * * @param {Number} columnIndex - The index of the column to extract as a row vector. * Must be a non-negative integer less than the matrix dimension. * @return {p5.Vector} A `p5.Vector` representing the extracted row of the matrix. * * @example * // META:norender * // Extracting a row vector from a 3x3 matrix * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const rowVector = matrix.row(1); // Returns a vector [2, 5, 8] * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const rowVector = matrix.row(1); // Extract the second row (index 1) * console.log("Row Vector:", rowVector.toString()); // Output: Row Vector: [2, 5, 8] * } */ row(columnIndex) { const columnVector = []; for (let i = 0; i < this.#sqDimention; i++) { columnVector.push(this.matrix[i * this.#sqDimention + columnIndex]); } return new Vector(...columnVector); } /** * A function that returns a column vector of a NxN matrix. * * This method extracts a specific column from the matrix and returns it as a `p5.Vector`. * The column is determined by the `rowIndex` parameter, which specifies the row index * of the matrix. This is useful for operations that require working with individual * columns of a matrix, such as column transformations or dot products. * * @param {Number} rowIndex - The index of the row to extract as a column vector. * Must be a non-negative integer less than the matrix dimension. * @return {p5.Vector} A `p5.Vector` representing the extracted column of the matrix. * * @example * // META:norender * // Extracting a column vector from a 3x3 matrix * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const columnVector = matrix.column(1); // Returns a vector [4, 5, 6] * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const columnVector = matrix.column(1); // Extract the second column (index 1) * console.log("Column Vector:", columnVector.toString()); // Output: Column Vector: [4, 5, 6] * } */ column(rowIndex) { const rowVector = []; for (let i = 0; i < this.#sqDimention; i++) { rowVector.push(this.matrix[rowIndex * this.#sqDimention + i]); } return new Vector(...rowVector); } /** * Transposes the given matrix `a` based on the square dimension of the matrix. * * This method rearranges the elements of the matrix such that the rows become columns * and the columns become rows. It handles matrices of different dimensions (4x4, 3x3, NxN) * by delegating to specific transpose methods for each case. * * If no argument is provided, the method transposes the current matrix instance. * If an argument is provided, it transposes the given matrix `a` and updates the current matrix. * * @param {Array} [a] - The matrix to be transposed. It should be a 2D array where each sub-array represents a row. * If omitted, the current matrix instance is transposed. * @returns {Matrix} - The current instance of the Matrix class, allowing for method chaining. * * @example * // META:norender * // Transposing a 3x3 matrix * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * matrix.transpose(); * console.log(matrix.matrix); // Output: [1, 4, 7, 2, 5, 8, 3, 6, 9] * * // Transposing a 4x4 matrix * const matrix4x4 = new p5.Matrix(4); * matrix4x4.transpose(); * console.log(matrix4x4.matrix); // Output: Transposed 4x4 identity matrix * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * console.log("Before transpose:", matrix.matrix); * matrix.transpose(); * console.log("After transpose:", matrix.matrix); // Output: [1, 4, 7, 2, 5, 8, 3, 6, 9] * } */ transpose(a) { if (this.#sqDimention === 4) { return this.#transpose4x4(a); } else if (this.#sqDimention === 3) { return this.#transpose3x3(a); } else { return this.#transposeNxN(a); } } /** * Multiplies the current matrix with another matrix or matrix-like array. * * This method supports several types of input: * - Another Matrix instance * - A matrix-like array (must be a perfect square, e.g., 4x4 or 3x3) * - Multiple arguments that form a perfect square matrix * * If the input is the same as the current matrix, a copy is made to avoid modifying the original matrix. * * The method determines the appropriate multiplication strategy based on the dimensions of the current matrix * and the input matrix. It supports 3x3, 4x4, and NxN matrices. * * @param {Matrix|Array|...number} multMatrix - The matrix or matrix-like array to multiply with. * @returns {Matrix|undefined} The resulting matrix after multiplication, or undefined if the input is invalid. * @chainable * * @example * // META:norender * // Multiplying two 3x3 matrices * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]); * matrix1.mult(matrix2); * console.log(matrix1.matrix); // Output: [30, 24, 18, 84, 69, 54, 138, 114, 90] * * // Multiplying a 4x4 matrix with another 4x4 matrix * const matrix4x4_1 = new p5.Matrix(4); // Identity matrix * const matrix4x4_2 = new p5.Matrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]); * matrix4x4_1.mult(matrix4x4_2); * console.log(matrix4x4_1.matrix); // Output: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1] * * @example * // META:norender * function setup() { * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]); * console.log("Before multiplication:", matrix1.matrix); * matrix1.mult(matrix2); * console.log("After multiplication:", matrix1.matrix); // Output: [30, 24, 18, 84, 69, 54, 138, 114, 90] * } */ mult(multMatrix) { let _src; if (multMatrix === this || multMatrix === this.matrix) { _src = this.copy().matrix; // only need to allocate in this rare case } else if (multMatrix instanceof Matrix) { _src = multMatrix.matrix; } else if (isMatrixArray(multMatrix) && isPerfectSquare(multMatrix)) { _src = multMatrix; } else if (isPerfectSquare(Array.from(arguments))) { _src = Array.from(arguments); } else ; if (this.#sqDimention === 4 && _src.length === 16) { return this.#mult4x4(_src); } else if (this.#sqDimention === 3 && _src.length === 9) { return this.#mult3x3(_src); } else { return this.#multNxN(_src); } } /** * Takes a vector and returns the vector resulting from multiplying to that vector by this matrix from left. This function is only for 3x3 matrices. * * This method applies the current 3x3 matrix to a given vector, effectively * transforming the vector using the matrix. The resulting vector is returned * as a new vector or stored in the provided target vector. * * @param {p5.Vector} multVector - The vector to which this matrix applies. * @param {p5.Vector} [target] - The vector to receive the result. If not provided, * a copy of the input vector will be created and returned. * @return {p5.Vector} - The transformed vector after applying the matrix. * * @example * // META:norender * // Multiplying a 3x3 matrix with a vector * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const vector = new p5.Vector(1, 2, 3); * const result = matrix.multiplyVec(vector); * console.log(result.toString()); // Output: Transformed vector * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const vector = new p5.Vector(1, 2, 3); * const result = matrix.multiplyVec(vector); * console.log("Original Vector:", vector.toString()); // Output : Original Vector: [1, 2, 3] * console.log("Transformed Vector:", result.toString()); // Output : Transformed Vector: [30, 36, 42] * } */ multiplyVec(multVector, target) { if (target === undefined) { target = multVector.copy(); } for (let i = 0; i < this.#sqDimention; i++) { target.values[i] = this.row(i).dot(multVector); } return target; } /** * Inverts a given matrix. * * This method inverts a matrix based on its dimensions. Currently, it supports * 3x3 and 4x4 matrices. If the matrix dimension is greater than 4, an error is thrown. * * For 4x4 matrices, it uses a specialized algorithm to compute the inverse. * For 3x3 matrices, it uses a different algorithm optimized for smaller matrices. * * If the matrix is singular (non-invertible), the method will return `null`. * * @param {Array} a - The matrix to be inverted. It should be a 2D array representing the matrix. * @returns {Array|null} - The inverted matrix, or `null` if the matrix is singular. * @throws {Error} - Throws an error if the matrix dimension is greater than 4. * * @example * // META:norender * // Inverting a 3x3 matrix * const matrix = new p5.Matrix([1, 2, 3, 0, 1, 4, 5, 6, 0]); * const invertedMatrix = matrix.invert(); * console.log(invertedMatrix.matrix); // Output: Inverted 3x3 matrix * * // Inverting a 4x4 matrix * const matrix4x4 = new p5.Matrix(4); // Identity matrix * matrix4x4.scale(2, 2, 2); * const invertedMatrix4x4 = matrix4x4.invert(); * console.log(invertedMatrix4x4.matrix); // Output: Inverted 4x4 matrix * * @example * // META:norender * function setup() { * * const matrix = new p5.Matrix([1, 2, 3, 0, 1, 4, 5, 6, 0]); * console.log("Original Matrix:", matrix.matrix); * const invertedMatrix = matrix.invert(); * if (invertedMatrix) { * console.log("Inverted Matrix:", invertedMatrix.matrix); * } else { * console.log("Matrix is singular and cannot be inverted."); * } * } */ invert(a) { if (this.#sqDimention === 4) { return this.#invert4x4(a); } else if (this.#sqDimention === 3) { return this.#invert3x3(a); } else { throw new Error( 'Invert is not implemented for N>4 at the moment, we are working on it' ); } } /** * Creates a 3x3 matrix whose entries are the top left 3x3 part and returns it. This function is only for 4x4 matrices. * * This method extracts the top-left 3x3 portion of a 4x4 matrix and creates a new * 3x3 matrix from it. This is particularly useful in 3D graphics for operations * that require only the rotational or scaling components of a transformation matrix. * * If the current matrix is not 4x4, an error is thrown to ensure the method is used * correctly. The resulting 3x3 matrix is independent of the original matrix, meaning * changes to the new matrix will not affect the original. * * @return {p5.Matrix} A new 3x3 matrix containing the top-left portion of the original 4x4 matrix. * @throws {Error} If the current matrix is not 4x4. * * @example * // META:norender * // Extracting a 3x3 submatrix from a 4x4 matrix * const matrix4x4 = new p5.Matrix(4); // Creates a 4x4 identity matrix * matrix4x4.scale(2, 2, 2); // Apply scaling transformation * const subMatrix3x3 = matrix4x4.createSubMatrix3x3(); * console.log("Original 4x4 Matrix:", matrix4x4.matrix); * console.log("Extracted 3x3 Submatrix:", subMatrix3x3.matrix); * * @example * // META:norender * function setup() { * const matrix4x4 = new p5.Matrix(4); // Creates a 4x4 identity matrix * matrix4x4.scale(2, 2, 2); // Apply scaling transformation * console.log("Original 4x4 Matrix:", matrix4x4.matrix); * * const subMatrix3x3 = matrix4x4.createSubMatrix3x3(); * console.log("Extracted 3x3 Submatrix:", subMatrix3x3.matrix); * } */ createSubMatrix3x3() { if (this.#sqDimention === 4) { const result = new Matrix(3); result.mat3[0] = this.matrix[0]; result.mat3[1] = this.matrix[1]; result.mat3[2] = this.matrix[2]; result.mat3[3] = this.matrix[4]; result.mat3[4] = this.matrix[5]; result.mat3[5] = this.matrix[6]; result.mat3[6] = this.matrix[8]; result.mat3[7] = this.matrix[9]; result.mat3[8] = this.matrix[10]; return result; } else { throw new Error('Matrix dimension must be 4 to create a 3x3 submatrix.'); } } /** * Converts a 4×4 matrix to its 3×3 inverse transpose transform. * This is commonly used in MVMatrix to NMatrix conversions, particularly * in 3D graphics for transforming normal vectors. * * This method extracts the top-left 3×3 portion of a 4×4 matrix, inverts it, * and then transposes the result. If the matrix is singular (non-invertible), * the resulting matrix will be zeroed out. * * @param {p5.Matrix} mat4 - The 4×4 matrix to be converted. * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining. * @throws {Error} If the current matrix is not 3×3. * * @example * // META:norender * // Converting a 4×4 matrix to its 3×3 inverse transpose * const mat4 = new p5.Matrix(4); // Create a 4×4 identity matrix * mat4.scale(2, 2, 2); // Apply scaling transformation * const mat3 = new p5.Matrix(3); // Create a 3×3 matrix * mat3.inverseTranspose4x4(mat4); * console.log("Converted 3×3 Matrix:", mat3.matrix); * * @example * // META:norender * function setup() { * const mat4 = new p5.Matrix(4); // Create a 4×4 identity matrix * mat4.scale(2, 2, 2); // Apply scaling transformation * console.log("Original 4×4 Matrix:", mat4.matrix); * * const mat3 = new p5.Matrix(3); // Create a 3×3 matrix * mat3.inverseTranspose4x4(mat4); * console.log("Converted 3×3 Matrix:", mat3.matrix); * } */ inverseTranspose4x4({ mat4 }) { if (this.#sqDimention !== 3) { throw new Error('This function only works with 3×3 matrices.'); } else { // Convert mat4 -> mat3 by extracting the top-left 3×3 portion this.matrix[0] = mat4[0]; this.matrix[1] = mat4[1]; this.matrix[2] = mat4[2]; this.matrix[3] = mat4[4]; this.matrix[4] = mat4[5]; this.matrix[5] = mat4[6]; this.matrix[6] = mat4[8]; this.matrix[7] = mat4[9]; this.matrix[8] = mat4[10]; } const inverse = this.invert(); // Check if inversion succeeded if (inverse) { inverse.transpose(this.matrix); } else { // In case of singularity, zero out the matrix for (let i = 0; i < 9; i++) { this.matrix[i] = 0; } } return this; } /** * Applies a transformation matrix to the current matrix. * * This method multiplies the current matrix by another matrix, which can be provided * in several forms: another Matrix instance, an array representing a matrix, or as * individual arguments representing the elements of a 4x4 matrix. * * This operation is useful for combining transformations such as translation, rotation, * scaling, and perspective projection into a single matrix. By applying a transformation * matrix, you can modify the current matrix to represent a new transformation. * * @param {Matrix|Array|number} multMatrix - The matrix to multiply with. This can be: * - An instance of the Matrix class. * - An array of 16 numbers representing a 4x4 matrix. * - 16 individual numbers representing the elements of a 4x4 matrix. * @returns {Matrix} The current matrix after applying the transformation. * * @example * // META:norender * function setup() { * // Assuming `matrix` is an instance of Matrix * const anotherMatrix = new p5.Matrix(4); * const anotherMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; * matrix.apply(anotherMatrix); * * // Applying a transformation using an array * const matrixArray = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; * matrix.apply(matrixArray); * * // Applying a transformation using individual arguments * matrix.apply(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); * * * // Create a 4x4 identity matrix * const matrix = new p5.Matrix(4); * console.log("Original Matrix:", matrix.matrix); * * // Create a scaling transformation matrix * const scalingMatrix = new p5.Matrix([2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]); * * // Apply the scaling transformation * matrix.apply(scalingMatrix); * console.log("After Scaling Transformation:", matrix.matrix); * * // Apply a translation transformation using an array * const translationMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 5, 5, 1]; * matrix.apply(translationMatrix); * console.log("After Translation Transformation:", matrix.matrix); * } */ apply(multMatrix) { let _src; if (multMatrix === this || multMatrix === this.matrix) { _src = this.copy().matrix; // only need to allocate in this rare case } else if (multMatrix instanceof Matrix) { _src = multMatrix.matrix; } else if (isMatrixArray(multMatrix)) { _src = multMatrix; } else if (arguments.length === 16) { _src = arguments; } else { return; // nothing to do. } const mat4 = this.matrix; // each row is used for the multiplier const m0 = mat4[0]; const m4 = mat4[4]; const m8 = mat4[8]; const m12 = mat4[12]; mat4[0] = _src[0] * m0 + _src[1] * m4 + _src[2] * m8 + _src[3] * m12; mat4[4] = _src[4] * m0 + _src[5] * m4 + _src[6] * m8 + _src[7] * m12; mat4[8] = _src[8] * m0 + _src[9] * m4 + _src[10] * m8 + _src[11] * m12; mat4[12] = _src[12] * m0 + _src[13] * m4 + _src[14] * m8 + _src[15] * m12; const m1 = mat4[1]; const m5 = mat4[5]; const m9 = mat4[9]; const m13 = mat4[13]; mat4[1] = _src[0] * m1 + _src[1] * m5 + _src[2] * m9 + _src[3] * m13; mat4[5] = _src[4] * m1 + _src[5] * m5 + _src[6] * m9 + _src[7] * m13; mat4[9] = _src[8] * m1 + _src[9] * m5 + _src[10] * m9 + _src[11] * m13; mat4[13] = _src[12] * m1 + _src[13] * m5 + _src[14] * m9 + _src[15] * m13; const m2 = mat4[2]; const m6 = mat4[6]; const m10 = mat4[10]; const m14 = mat4[14]; mat4[2] = _src[0] * m2 + _src[1] * m6 + _src[2] * m10 + _src[3] * m14; mat4[6] = _src[4] * m2 + _src[5] * m6 + _src[6] * m10 + _src[7] * m14; mat4[10] = _src[8] * m2 + _src[9] * m6 + _src[10] * m10 + _src[11] * m14; mat4[14] = _src[12] * m2 + _src[13] * m6 + _src[14] * m10 + _src[15] * m14; const m3 = mat4[3]; const m7 = mat4[7]; const m11 = mat4[11]; const m15 = mat4[15]; mat4[3] = _src[0] * m3 + _src[1] * m7 + _src[2] * m11 + _src[3] * m15; mat4[7] = _src[4] * m3 + _src[5] * m7 + _src[6] * m11 + _src[7] * m15; mat4[11] = _src[8] * m3 + _src[9] * m7 + _src[10] * m11 + _src[11] * m15; mat4[15] = _src[12] * m3 + _src[13] * m7 + _src[14] * m11 + _src[15] * m15; return this; } /** * Scales a p5.Matrix by scalars or a vector. * * This method applies a scaling transformation to the current matrix. * Scaling is a transformation that enlarges or shrinks objects by a scale factor * along the x, y, and z axes. The scale factors can be provided as individual * numbers, an array, or a `p5.Vector`. * * If a `p5.Vector` or an array is provided, the x, y, and z components are extracted * from it. If the z component is not provided, it defaults to 1 (no scaling along the z-axis). * * @param {p5.Vector|Float32Array|Number[]} s - The vector or scalars to scale by. * Can be a `p5.Vector`, an array, or individual numbers. * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining. * * @example * // META:norender * // Scaling a matrix by individual scalars * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * matrix.scale(2, 3, 4); // Scale by 2 along x, 3 along y, and 4 along z * console.log(matrix.matrix); * * // Scaling a matrix by a p5.Vector * const scaleVector = new p5.Vector(2, 3, 4); * matrix.scale(scaleVector); * console.log(matrix.matrix); * * // Scaling a matrix by an array * const scaleArray = [2, 3, 4]; * matrix.scale(scaleArray); * console.log(matrix.matrix); * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Scale the matrix by individual scalars * matrix.scale(2, 3, 4); * console.log("Scaled Matrix (2, 3, 4):", matrix.matrix); * * // Scale the matrix by a p5.Vector * const scaleVector = new p5.Vector(1.5, 2.5, 3.5); * matrix.scale(scaleVector); * console.log("Scaled Matrix (Vector):", matrix.matrix); * * // Scale the matrix by an array * const scaleArray = [0.5, 0.5, 0.5]; * matrix.scale(scaleArray); * console.log("Scaled Matrix (Array):", matrix.matrix); * } */ scale(x, y, z) { if (x instanceof Vector) { // x is a vector, extract the components from it. y = x.y; z = x.z; x = x.x; // must be last } else if (x instanceof Array) { // x is an array, extract the components from it. y = x[1]; z = x[2]; x = x[0]; // must be last } this.matrix[0] *= x; this.matrix[1] *= x; this.matrix[2] *= x; this.matrix[3] *= x; this.matrix[4] *= y; this.matrix[5] *= y; this.matrix[6] *= y; this.matrix[7] *= y; this.matrix[8] *= z; this.matrix[9] *= z; this.matrix[10] *= z; this.matrix[11] *= z; return this; } /** * Rotate the Matrix around a specified axis by a given angle. * * This method applies a rotation transformation to the matrix, modifying its orientation * in 3D space. The rotation is performed around the provided axis, which can be defined * as a `p5.Vector` or an array of numbers representing the x, y, and z components of the axis. * Rotate our Matrix around an axis by the given angle. * @param {Number} a The angle of rotation in radians. * Angles in radians are a measure of rotation, where 2π radians * represent a full circle (360 degrees). For example: * - π/2 radians = 90 degrees (quarter turn) * - π radians = 180 degrees (half turn) * - 2π radians = 360 degrees (full turn) * Use `Math.PI` for π or `p5`'s `PI` constant if using p5.js. * @param {p5.Vector|Number[]} axis The axis or axes to rotate around. * This defines the direction of the rotation. * - If using a `p5.Vector`, it should represent * the x, y, and z components of the axis. * - If using an array, it should be in the form * [x, y, z], where x, y, and z are numbers. * For example: * - [1, 0, 0] rotates around the x-axis. * - [0, 1, 0] rotates around the y-axis. * - [0, 0, 1] rotates around the z-axis. * * @chainable * inspired by Toji's gl-matrix lib, mat4 rotation * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] * * // Translate the matrix by a 3D vector * matrix.rotate4x4(Math.PI, [1,0,0]); * console.log("After rotation of PI degrees on vector [1,0,0]:", matrix.matrix.slice().toString()); // [1,0,0,0,0,-1,1.2246468525851679e-16,0,0,-1.2246468525851679e-16,-1,0,0,0,0,1] * } */ rotate4x4(a, x, y, z) { if (x instanceof Vector) { // x is a vector, extract the components from it. y = x.y; z = x.z; x = x.x; //must be last } else if (x instanceof Array) { // x is an array, extract the components from it. y = x[1]; z = x[2]; x = x[0]; //must be last } const len = Math.sqrt(x * x + y * y + z * z); x *= 1 / len; y *= 1 / len; z *= 1 / len; const a00 = this.matrix[0]; const a01 = this.matrix[1]; const a02 = this.matrix[2]; const a03 = this.matrix[3]; const a10 = this.matrix[4]; const a11 = this.matrix[5]; const a12 = this.matrix[6]; const a13 = this.matrix[7]; const a20 = this.matrix[8]; const a21 = this.matrix[9]; const a22 = this.matrix[10]; const a23 = this.matrix[11]; //sin,cos, and tan of respective angle const sA = Math.sin(a); const cA = Math.cos(a); const tA = 1 - cA; // Construct the elements of the rotation matrix const b00 = x * x * tA + cA; const b01 = y * x * tA + z * sA; const b02 = z * x * tA - y * sA; const b10 = x * y * tA - z * sA; const b11 = y * y * tA + cA; const b12 = z * y * tA + x * sA; const b20 = x * z * tA + y * sA; const b21 = y * z * tA - x * sA; const b22 = z * z * tA + cA; // rotation-specific matrix multiplication this.matrix[0] = a00 * b00 + a10 * b01 + a20 * b02; this.matrix[1] = a01 * b00 + a11 * b01 + a21 * b02; this.matrix[2] = a02 * b00 + a12 * b01 + a22 * b02; this.matrix[3] = a03 * b00 + a13 * b01 + a23 * b02; this.matrix[4] = a00 * b10 + a10 * b11 + a20 * b12; this.matrix[5] = a01 * b10 + a11 * b11 + a21 * b12; this.matrix[6] = a02 * b10 + a12 * b11 + a22 * b12; this.matrix[7] = a03 * b10 + a13 * b11 + a23 * b12; this.matrix[8] = a00 * b20 + a10 * b21 + a20 * b22; this.matrix[9] = a01 * b20 + a11 * b21 + a21 * b22; this.matrix[10] = a02 * b20 + a12 * b21 + a22 * b22; this.matrix[11] = a03 * b20 + a13 * b21 + a23 * b22; return this; } /** * Translates the current matrix by a given vector. * * This method applies a translation transformation to the current matrix. * Translation moves the matrix by a specified amount along the x, y, and z axes. * The input vector can be a 2D or 3D vector. If the z-component is not provided, * it defaults to 0, meaning no translation along the z-axis. * * @param {Number[]} v - A vector representing the translation. It should be an array * with two or three elements: [x, y, z]. The z-component is optional. * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining. * * @example * // META:norender * // Translating a matrix by a 3D vector * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * matrix.translate([10, 20, 30]); // Translate by 10 units along x, 20 along y, and 30 along z * console.log(matrix.matrix); * * // Translating a matrix by a 2D vector * matrix.translate([5, 15]); // Translate by 5 units along x and 15 along y * console.log(matrix.matrix); * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] * * // Translate the matrix by a 3D vector * matrix.translate([10, 20, 30]); * console.log("After 3D Translation (10, 20, 30):", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,10,20,30,1] * * // Translate the matrix by a 2D vector * matrix.translate([5, 15]); * console.log("After 2D Translation (5, 15):", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,15,35,30,1] * } */ translate(v) { const x = v[0], y = v[1], z = v[2] || 0; this.matrix[12] += this.matrix[0] * x + this.matrix[4] * y + this.matrix[8] * z; this.matrix[13] += this.matrix[1] * x + this.matrix[5] * y + this.matrix[9] * z; this.matrix[14] += this.matrix[2] * x + this.matrix[6] * y + this.matrix[10] * z; this.matrix[15] += this.matrix[3] * x + this.matrix[7] * y + this.matrix[11] * z; return this; } /** * Rotates the matrix around the X-axis by a given angle. * * This method modifies the current matrix to apply a rotation transformation * around the X-axis. The rotation angle is specified in radians. * * Rotating around the X-axis means that the Y and Z coordinates of the matrix * are transformed while the X coordinates remain unchanged. This is commonly * used in 3D graphics to create animations or transformations along the X-axis. * * @param {Number} a - The angle in radians to rotate the matrix by. * * @example * // META:norender * // Rotating a matrix around the X-axis * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * matrix.rotateX(Math.PI / 4); // Rotate 45 degrees around the X-axis * console.log(matrix.matrix); * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Rotate the matrix 45 degrees (PI/4 radians) around the X-axis * matrix.rotateX(Math.PI / 4); * console.log("After Rotation (X-axis, 45 degrees):", matrix.matrix); * } */ rotateX(a) { this.rotate4x4(a, 1, 0, 0); } /** * Rotates the matrix around the Y-axis by a given angle. * * This method modifies the current matrix to apply a rotation transformation * around the Y-axis. The rotation is performed in 3D space, and the angle * is specified in radians. Rotating around the Y-axis means that the X and Z * coordinates of the matrix are transformed while the Y coordinates remain * unchanged. This is commonly used in 3D graphics to create animations or * transformations along the Y-axis. * * @param {Number} a - The angle in radians to rotate the matrix by. Positive * values rotate the matrix counterclockwise, and negative values rotate it * clockwise. * * @example * // META:norender * // Rotating a matrix around the Y-axis * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * matrix.rotateY(Math.PI / 4); // Rotate 45 degrees around the Y-axis * console.log(matrix.matrix); * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Rotate the matrix 45 degrees (PI/4 radians) around the Y-axis * matrix.rotateY(Math.PI / 4); * console.log("After Rotation (Y-axis, 45 degrees):", matrix.matrix); * } */ rotateY(a) { this.rotate4x4(a, 0, 1, 0); } /** * Rotates the matrix around the Z-axis by a given angle. * * This method modifies the current matrix to apply a rotation transformation * around the Z-axis. The rotation is performed in a 4x4 matrix context, which * is commonly used in 3D graphics to handle transformations. Rotating around * the Z-axis means that the X and Y coordinates of the matrix are transformed * while the Z coordinates remain unchanged. * * @param {Number} a - The angle in radians to rotate the matrix by. Positive * values rotate the matrix counterclockwise, and negative values rotate it * clockwise. * * @returns {Matrix} The current instance of the Matrix class, allowing for * method chaining. * * @example * // META:norender * // Rotating a matrix around the Z-axis * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * matrix.rotateZ(Math.PI / 4); // Rotate 45 degrees around the Z-axis * console.log(matrix.matrix); * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Rotate the matrix 45 degrees (PI/4 radians) around the Z-axis * matrix.rotateZ(Math.PI / 4); * console.log("After Rotation (Z-axis, 45 degrees):", matrix.matrix); * } */ rotateZ(a) { this.rotate4x4(a, 0, 0, 1); } /** * Sets the perspective projection matrix. * * This method modifies the current matrix to represent a perspective projection. * Perspective projection is commonly used in 3D graphics to simulate the effect * of objects appearing smaller as they move further away from the camera. * * The perspective matrix is defined by the field of view (fovy), aspect ratio, * and the near and far clipping planes. The near and far clipping planes define * the range of depth that will be rendered, with anything outside this range * being clipped. * * @param {Number} fovy - The field of view in the y direction, in radians. * @param {Number} aspect - The aspect ratio of the viewport (width / height). * @param {Number} near - The distance to the near clipping plane. Must be greater than 0. * @param {Number} far - The distance to the far clipping plane. Must be greater than the near value. * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining. * * @example * // META:norender * // Setting a perspective projection matrix * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * matrix.perspective(Math.PI / 4, 1.5, 0.1, 100); // Set perspective projection * console.log(matrix.matrix); * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Set a perspective projection with a 45-degree field of view, * // an aspect ratio of 1.5, and near/far clipping planes at 0.1 and 100. * matrix.perspective(Math.PI / 4, 1.5, 0.1, 100); * console.log("Perspective Matrix:", matrix.matrix); * } */ perspective(fovy, aspect, near, far) { const f = 1.0 / Math.tan(fovy / 2), nf = 1 / (near - far); this.matrix[0] = f / aspect; this.matrix[1] = 0; this.matrix[2] = 0; this.matrix[3] = 0; this.matrix[4] = 0; this.matrix[5] = f; this.matrix[6] = 0; this.matrix[7] = 0; this.matrix[8] = 0; this.matrix[9] = 0; this.matrix[10] = (far + near) * nf; this.matrix[11] = -1; this.matrix[12] = 0; this.matrix[13] = 0; this.matrix[14] = 2 * far * near * nf; this.matrix[15] = 0; return this; } /** * Sets this matrix to an orthographic projection matrix. * * An orthographic projection matrix is used to create a 2D rendering * of a 3D scene by projecting points onto a plane without perspective * distortion. This method modifies the current matrix to represent * the orthographic projection defined by the given parameters. * * @param {number} left - The coordinate for the left vertical clipping plane. * @param {number} right - The coordinate for the right vertical clipping plane. * @param {number} bottom - The coordinate for the bottom horizontal clipping plane. * @param {number} top - The coordinate for the top horizontal clipping plane. * @param {number} near - The distance to the near depth clipping plane. Must be positive. * @param {number} far - The distance to the far depth clipping plane. Must be positive. * @chainable * @returns {Matrix} The current matrix instance, updated with the orthographic projection. * * @example * // META:norender * // Example using p5.js to demonstrate orthographic projection * function setup() { * let orthoMatrix = new p5.Matrix(4); * console.log(orthoMatrix.matrix.toString()) // Output: 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 * orthoMatrix.ortho(-200, 200, -200, 200, 0.1, 1000); * console.log(orthoMatrix.matrix.toString()) // Output: [24 0.004999999888241291,0,0,0,0,0.004999999888241291,0,0,0,0,-0.0020002000965178013,0,0,0,-1.0002000331878662,1] * applyMatrix( * orthoMatrix.mat4[0], orthoMatrix.mat4[1], orthoMatrix.mat4[2], orthoMatrix.mat4[3], * orthoMatrix.mat4[4], orthoMatrix.mat4[5], orthoMatrix.mat4[6], orthoMatrix.mat4[7], * orthoMatrix.mat4[8], orthoMatrix.mat4[9], orthoMatrix.mat4[10], orthoMatrix.mat4[11], * orthoMatrix.mat4[12], orthoMatrix.mat4[13], orthoMatrix.mat4[14], orthoMatrix.mat4[15] * ); * console.log(orthoMatrix.matrix.toString()) // Output: [31 0.004999999888241291,0,0,0,0,0.004999999888241291,0,0,0,0,-0.0020002000965178013,0,0,0,-1.0002000331878662,1] * } */ ortho(left, right, bottom, top, near, far) { const lr = 1 / (left - right), bt = 1 / (bottom - top), nf = 1 / (near - far); this.matrix[0] = -2 * lr; this.matrix[1] = 0; this.matrix[2] = 0; this.matrix[3] = 0; this.matrix[4] = 0; this.matrix[5] = -2 * bt; this.matrix[6] = 0; this.matrix[7] = 0; this.matrix[8] = 0; this.matrix[9] = 0; this.matrix[10] = 2 * nf; this.matrix[11] = 0; this.matrix[12] = (left + right) * lr; this.matrix[13] = (top + bottom) * bt; this.matrix[14] = (far + near) * nf; this.matrix[15] = 1; return this; } /** * Applies a matrix to a vector with x, y, z, w components and returns the result as an array. * * This method multiplies the current matrix by a 4D vector (x, y, z, w) and computes the resulting vector. * It is commonly used in 3D graphics for transformations such as translation, rotation, scaling, and perspective projection. * * The resulting vector is returned as an array of four numbers, representing the transformed x, y, z, and w components. * * @param {Number} x - The x component of the vector. * @param {Number} y - The y component of the vector. * @param {Number} z - The z component of the vector. * @param {Number} w - The w component of the vector. * @returns {Number[]} An array containing the transformed [x, y, z, w] components. * * @example * // META:norender * // Applying a matrix to a 4D vector * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * const result = matrix.multiplyVec4(1, 2, 3, 1); // Transform the vector [1, 2, 3, 1] * console.log(result); // Output: [1, 2, 3, 1] (unchanged for identity matrix) * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Apply the matrix to a 4D vector * const result = matrix.multiplyVec4(1, 2, 3, 1); * console.log("Transformed Vector:", result); // Output: [1, 2, 3, 1] * * // Modify the matrix (e.g., apply a translation) * matrix.translate([5, 5, 5]); * console.log("Modified Matrix:", matrix.matrix); * * // Apply the modified matrix to the same vector * const transformedResult = matrix.multiplyVec4(1, 2, 3, 1); * console.log("Transformed Vector after Translation:", transformedResult); // Output: [6, 7, 8, 1] * } */ multiplyVec4(x, y, z, w) { const result = new Array(4); const m = this.matrix; result[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; result[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; result[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; result[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; return result; } /** * Applies a matrix to a vector. The fourth component is set to 1. * Returns a vector consisting of the first * through third components of the result. * * This method multiplies the current matrix by a 4D vector (x, y, z, 1), * effectively transforming the vector using the matrix. The resulting * vector is returned as a new `p5.Vector` instance. * * This is useful for applying transformations such as translation, * rotation, scaling, or perspective projection to a point in 3D space. * * @param {p5.Vector} vector - The input vector to transform. It should * have x, y, and z components. * @return {p5.Vector} A new `p5.Vector` instance representing the transformed point. * * @example * // META:norender * // Applying a matrix to a 3D point * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * const point = new p5.Vector(1, 2, 3); // Define a 3D point * const transformedPoint = matrix.multiplyPoint(point); * console.log(transformedPoint.toString()); // Output: [1, 2, 3] (unchanged for identity matrix) * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Define a 3D point * const point = new p5.Vector(1, 2, 3); * console.log("Original Point:", point.toString()); * * // Apply the matrix to the point * const transformedPoint = matrix.multiplyPoint(point); * console.log("Transformed Point:", transformedPoint.toString()); * * // Modify the matrix (e.g., apply a translation) * matrix.translate([5, 5, 5]); * console.log("Modified Matrix:", matrix.matrix); * * // Apply the modified matrix to the same point * const translatedPoint = matrix.multiplyPoint(point); * console.log("Translated Point:", translatedPoint.toString()); // Output: [6, 7, 8] * } */ multiplyPoint({ x, y, z }) { const array = this.multiplyVec4(x, y, z, 1); return new Vector(array[0], array[1], array[2]); } /** * Applies a matrix to a vector. * The fourth component is set to 1. * Returns the result of dividing the 1st to 3rd components * of the result by the 4th component as a vector. * * This method multiplies the current matrix by a 4D vector (x, y, z, 1), * effectively transforming the vector using the matrix. The resulting * vector is normalized by dividing its x, y, and z components by the w component. * This is useful for applying transformations such as perspective projection * to a point in 3D space. * * @param {p5.Vector} vector - The input vector to transform. It should * have x, y, and z components. * @return {p5.Vector} A new `p5.Vector` instance representing the transformed and normalized point. * * @example * // META:norender * // Applying a matrix to a 3D point and normalizing it * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * const point = new p5.Vector(1, 2, 3); // Define a 3D point * const transformedPoint = matrix.multiplyAndNormalizePoint(point); * console.log(transformedPoint.toString()); // Output: [1, 2, 3] (unchanged for identity matrix) * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Define a 3D point * const point = new p5.Vector(1, 2, 3); * console.log("Original Point:", point.toString()); * * // Apply the matrix to the point and normalize it * const transformedPoint = matrix.multiplyAndNormalizePoint(point); * console.log("Transformed and Normalized Point:", transformedPoint.toString()); * * // Modify the matrix (e.g., apply a perspective transformation) * matrix.perspective(Math.PI / 4, 1.5, 0.1, 100); * console.log("Modified Matrix (Perspective):", matrix.matrix); * * // Apply the modified matrix to the same point * const perspectivePoint = matrix.multiplyAndNormalizePoint(point); * console.log("Point after Perspective Transformation:", perspectivePoint.toString()); * } */ multiplyAndNormalizePoint({ x, y, z }) { const array = this.multiplyVec4(x, y, z, 1); array[0] /= array[3]; array[1] /= array[3]; array[2] /= array[3]; return new Vector(array[0], array[1], array[2]); } /** * Applies a matrix to a vector. * The fourth component is set to 0. * Returns a vector consisting of the first * through third components of the result. * * This method multiplies the current matrix by a 4D vector (x, y, z, 0), * effectively transforming the direction vector using the matrix. The resulting * vector is returned as a new `p5.Vector` instance. This is particularly useful * for transforming direction vectors (e.g., normals) without applying translation. * * @param {p5.Vector} vector - The input vector to transform. It should * have x, y, and z components. * @return {p5.Vector} A new `p5.Vector` instance representing the transformed direction. * * @example * // META:norender * // Applying a matrix to a direction vector * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * const direction = new p5.Vector(1, 0, 0); // Define a direction vector * const transformedDirection = matrix.multiplyDirection(direction); * console.log(transformedDirection.toString()); // Output: [1, 0, 0] (unchanged for identity matrix) * * @example * // META:norender * function setup() { * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix * console.log("Original Matrix:", matrix.matrix); * * // Define a direction vector * const direction = new p5.Vector(1, 0, 0); * console.log("Original Direction:", direction.toString()); * * // Apply the matrix to the direction vector * const transformedDirection = matrix.multiplyDirection(direction); * console.log("Transformed Direction:", transformedDirection.toString()); * * // Modify the matrix (e.g., apply a rotation) * matrix.rotateY(Math.PI / 4); // Rotate 45 degrees around the Y-axis * console.log("Modified Matrix (Rotation):", matrix.matrix); * * // Apply the modified matrix to the same direction vector * const rotatedDirection = matrix.multiplyDirection(direction); * console.log("Rotated Direction:", rotatedDirection.toString()); // Output: Rotated vector * } */ multiplyDirection({ x, y, z }) { const array = this.multiplyVec4(x, y, z, 0); return new Vector(array[0], array[1], array[2]); } /** * Takes a vector and returns the vector resulting from multiplying. This function is only for 3x3 matrices. * that vector by this matrix from the left. * * This method applies the current 3x3 matrix to a given vector, effectively * transforming the vector using the matrix. The resulting vector is returned * as a new vector or stored in the provided target vector. * * This is useful for operations such as transforming points or directions * in 2D or 3D space using a 3x3 transformation matrix. * * @param {p5.Vector} multVector - The vector to which this matrix applies. * @param {p5.Vector} [target] - The vector to receive the result. If not provided, * a copy of the input vector will be created and returned. * @return {p5.Vector} - The transformed vector after applying the matrix. * * @example * // META:norender * // Multiplying a 3x3 matrix with a vector * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const vector = new p5.Vector(1, 2, 3); * const result = matrix.multiplyVec3(vector); * console.log(result.toString()); // Output: Transformed vector * * @example * // META:norender * function setup() { * // Create a 3x3 matrix * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * console.log("Original Matrix:", matrix.matrix); * * // Define a vector * const vector = new p5.Vector(1, 2, 3); * console.log("Original Vector:", vector.toString()); // Output: [1, 2, 3] * * // Apply the matrix to the vector * const transformedVector = matrix.multiplyVec3(vector); * console.log("Transformed Vector:", transformedVector.toString()); // Output: [30, 36, 42] * * // Modify the matrix (e.g., apply a scaling transformation) * matrix.scale(2, 2, 2); * console.log("Modified Matrix (Scaling):", matrix.matrix); // Output: [2, 4, 6, 8, 10, 12, 14, 16, 18] * * // Apply the modified matrix to the same vector * const scaledVector = matrix.multiplyVec3(vector); * console.log("Scaled Vector:", scaledVector.toString()); // Output: [60, 72, 84] * } */ multiplyVec3(multVector, target) { if (target === undefined) { target = multVector.copy(); } target.x = this.row(0).dot(multVector); target.y = this.row(1).dot(multVector); target.z = this.row(2).dot(multVector); return target; } // ==================== // PRIVATE /** * Creates identity matrix * This method updates the current matrix with the result of the multiplication. * * @private */ #createIdentityMatrix(dimension) { // This it to prevent loops in the most common 3x3 and 4x4 cases // TODO: check performance if it actually helps if (dimension === 3) return new GLMAT_ARRAY_TYPE([1, 0, 0, 0, 1, 0, 0, 0, 1]); if (dimension === 4) return new GLMAT_ARRAY_TYPE([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); const identityMatrix = new GLMAT_ARRAY_TYPE(dimension * dimension).fill(0); for (let i = 0; i < dimension; i++) { identityMatrix[i * dimension + i] = 1; } return identityMatrix; } /** * Multiplies the current 4x4 matrix with another 4x4 matrix. * This method updates the current matrix with the result of the multiplication. * * @private * @param {number[]} _src - A 16-element array representing the 4x4 matrix to multiply with. * * @returns {this} The current instance with the updated matrix. * * @example * // META:norender * // Assuming `matrix` is an instance of the Matrix class * const srcMatrix = [ * 1, 0, 0, 0, * 0, 1, 0, 0, * 0, 0, 1, 0, * 0, 0, 0, 1 * ]; * matrix.#mult4x4(srcMatrix); */ #mult4x4(_src) { // each row is used for the multiplier let b0 = this.matrix[0], b1 = this.matrix[1], b2 = this.matrix[2], b3 = this.matrix[3]; this.matrix[0] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; this.matrix[1] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; this.matrix[2] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; this.matrix[3] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; b0 = this.matrix[4]; b1 = this.matrix[5]; b2 = this.matrix[6]; b3 = this.matrix[7]; this.matrix[4] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; this.matrix[5] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; this.matrix[6] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; this.matrix[7] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; b0 = this.matrix[8]; b1 = this.matrix[9]; b2 = this.matrix[10]; b3 = this.matrix[11]; this.matrix[8] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; this.matrix[9] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; this.matrix[10] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; this.matrix[11] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; b0 = this.matrix[12]; b1 = this.matrix[13]; b2 = this.matrix[14]; b3 = this.matrix[15]; this.matrix[12] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; this.matrix[13] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; this.matrix[14] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; this.matrix[15] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; return this; } /** * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix * we want to multiply by * @private * @chainable */ #multNxN(multMatrix) { if (multMatrix.length !== this.matrix.length) { throw new Error('Matrices must be of the same dimension to multiply.'); } const result = new GLMAT_ARRAY_TYPE(this.matrix.length).fill(0); for (let i = 0; i < this.#sqDimention; i++) { for (let j = 0; j < this.#sqDimention; j++) { for (let k = 0; k < this.#sqDimention; k++) { result[i * this.#sqDimention + j] += this.matrix[i * this.#sqDimention + k] * multMatrix[k * this.#sqDimention + j]; } } } this.matrix = result; return this; } /** * This function is only for 3x3 matrices. * multiply two mat3s. It is an operation to multiply the 3x3 matrix of * the argument from the right. Arguments can be a 3x3 p5.Matrix, * a Float32Array of length 9, or a javascript array of length 9. * In addition, it can also be done by enumerating 9 numbers. * * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix * we want to multiply by * @private * @chainable */ #mult3x3(_src) { // each row is used for the multiplier let b0 = this.mat3[0]; let b1 = this.mat3[1]; let b2 = this.mat3[2]; this.mat3[0] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6]; this.mat3[1] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7]; this.mat3[2] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8]; b0 = this.mat3[3]; b1 = this.mat3[4]; b2 = this.mat3[5]; this.mat3[3] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6]; this.mat3[4] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7]; this.mat3[5] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8]; b0 = this.mat3[6]; b1 = this.mat3[7]; b2 = this.mat3[8]; this.mat3[6] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6]; this.mat3[7] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7]; this.mat3[8] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8]; return this; } /** * Transposes a square matrix in place. * This method swaps the rows and columns of the matrix, effectively flipping it over its diagonal. * * @private * @returns {Matrix} The current instance of the Matrix, with the transposed values. */ #transposeNxN() { const n = this.#sqDimention; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { this.matrix[i * n + j] = this.matrix[j * n + i]; } } return this; } /** * transpose according to a given matrix * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be * based on to transpose * @private * @chainable */ #transpose4x4(a) { console.log('====> 4x4'); let a01, a02, a03, a12, a13, a23; if (a instanceof Matrix) { a01 = a.matrix[1]; a02 = a.matrix[2]; a03 = a.matrix[3]; a12 = a.matrix[6]; a13 = a.matrix[7]; a23 = a.matrix[11]; this.matrix[0] = a.matrix[0]; this.matrix[1] = a.matrix[4]; this.matrix[2] = a.matrix[8]; this.matrix[3] = a.matrix[12]; this.matrix[4] = a01; this.matrix[5] = a.matrix[5]; this.matrix[6] = a.matrix[9]; this.matrix[7] = a.matrix[13]; this.matrix[8] = a02; this.matrix[9] = a12; this.matrix[10] = a.matrix[10]; this.matrix[11] = a.matrix[14]; this.matrix[12] = a03; this.matrix[13] = a13; this.matrix[14] = a23; this.matrix[15] = a.matrix[15]; } else if (isMatrixArray(a)) { a01 = a[1]; a02 = a[2]; a03 = a[3]; a12 = a[6]; a13 = a[7]; a23 = a[11]; this.matrix[0] = a[0]; this.matrix[1] = a[4]; this.matrix[2] = a[8]; this.matrix[3] = a[12]; this.matrix[4] = a01; this.matrix[5] = a[5]; this.matrix[6] = a[9]; this.matrix[7] = a[13]; this.matrix[8] = a02; this.matrix[9] = a12; this.matrix[10] = a[10]; this.matrix[11] = a[14]; this.matrix[12] = a03; this.matrix[13] = a13; this.matrix[14] = a23; this.matrix[15] = a[15]; } return this; } /** * This function is only for 3x3 matrices. * transposes a 3×3 p5.Matrix by a mat3 * If there is an array of arguments, the matrix obtained by transposing * the 3x3 matrix generated based on that array is set. * If no arguments, it transposes itself and returns it. * * @param {Number[]} mat3 1-dimensional array * @private * @chainable */ #transpose3x3(mat3) { if (mat3 === undefined) { mat3 = this.mat3; } const a01 = mat3[1]; const a02 = mat3[2]; const a12 = mat3[5]; this.mat3[0] = mat3[0]; this.mat3[1] = mat3[3]; this.mat3[2] = mat3[6]; this.mat3[3] = a01; this.mat3[4] = mat3[4]; this.mat3[5] = mat3[7]; this.mat3[6] = a02; this.mat3[7] = a12; this.mat3[8] = mat3[8]; return this; } /** * Only 4x4 becasuse determinant is only 4x4 currently * invert matrix according to a give matrix * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be * based on to invert * @private * @chainable */ #invert4x4(a) { let a00, a01, a02, a03, a10, a11, a12, a13; let a20, a21, a22, a23, a30, a31, a32, a33; if (a instanceof Matrix) { a00 = a.matrix[0]; a01 = a.matrix[1]; a02 = a.matrix[2]; a03 = a.matrix[3]; a10 = a.matrix[4]; a11 = a.matrix[5]; a12 = a.matrix[6]; a13 = a.matrix[7]; a20 = a.matrix[8]; a21 = a.matrix[9]; a22 = a.matrix[10]; a23 = a.matrix[11]; a30 = a.matrix[12]; a31 = a.matrix[13]; a32 = a.matrix[14]; a33 = a.matrix[15]; } else if (isMatrixArray(a)) { a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; a30 = a[12]; a31 = a[13]; a32 = a[14]; a33 = a[15]; } const b00 = a00 * a11 - a01 * a10; const b01 = a00 * a12 - a02 * a10; const b02 = a00 * a13 - a03 * a10; const b03 = a01 * a12 - a02 * a11; const b04 = a01 * a13 - a03 * a11; const b05 = a02 * a13 - a03 * a12; const b06 = a20 * a31 - a21 * a30; const b07 = a20 * a32 - a22 * a30; const b08 = a20 * a33 - a23 * a30; const b09 = a21 * a32 - a22 * a31; const b10 = a21 * a33 - a23 * a31; const b11 = a22 * a33 - a23 * a32; // Calculate the determinant let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (!det) { return null; } det = 1.0 / det; this.matrix[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; this.matrix[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; this.matrix[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; this.matrix[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; this.matrix[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; this.matrix[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; this.matrix[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; this.matrix[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; this.matrix[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; this.matrix[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; this.matrix[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; this.matrix[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; this.matrix[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; this.matrix[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; this.matrix[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; this.matrix[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; return this; } /** * Inverts a 3×3 matrix * @chainable * @private */ #invert3x3() { const a00 = this.mat3[0]; const a01 = this.mat3[1]; const a02 = this.mat3[2]; const a10 = this.mat3[3]; const a11 = this.mat3[4]; const a12 = this.mat3[5]; const a20 = this.mat3[6]; const a21 = this.mat3[7]; const a22 = this.mat3[8]; const b01 = a22 * a11 - a12 * a21; const b11 = -a22 * a10 + a12 * a20; const b21 = a21 * a10 - a11 * a20; // Calculate the determinant let det = a00 * b01 + a01 * b11 + a02 * b21; if (!det) { return null; } det = 1.0 / det; this.mat3[0] = b01 * det; this.mat3[1] = (-a22 * a01 + a02 * a21) * det; this.mat3[2] = (a12 * a01 - a02 * a11) * det; this.mat3[3] = b11 * det; this.mat3[4] = (a22 * a00 - a02 * a20) * det; this.mat3[5] = (-a12 * a00 + a02 * a10) * det; this.mat3[6] = b21 * det; this.mat3[7] = (-a21 * a00 + a01 * a20) * det; this.mat3[8] = (a11 * a00 - a01 * a10) * det; return this; } /** * inspired by Toji's mat4 determinant * @return {Number} Determinant of our 4×4 matrix * @private */ #determinant4x4() { if (this.#sqDimention !== 4) { throw new Error( 'Determinant is only implemented for 4x4 matrices. We are working on it.' ); } const d00 = this.matrix[0] * this.matrix[5] - this.matrix[1] * this.matrix[4], d01 = this.matrix[0] * this.matrix[6] - this.matrix[2] * this.matrix[4], d02 = this.matrix[0] * this.matrix[7] - this.matrix[3] * this.matrix[4], d03 = this.matrix[1] * this.matrix[6] - this.matrix[2] * this.matrix[5], d04 = this.matrix[1] * this.matrix[7] - this.matrix[3] * this.matrix[5], d05 = this.matrix[2] * this.matrix[7] - this.matrix[3] * this.matrix[6], d06 = this.matrix[8] * this.matrix[13] - this.matrix[9] * this.matrix[12], d07 = this.matrix[8] * this.matrix[14] - this.matrix[10] * this.matrix[12], d08 = this.matrix[8] * this.matrix[15] - this.matrix[11] * this.matrix[12], d09 = this.matrix[9] * this.matrix[14] - this.matrix[10] * this.matrix[13], d10 = this.matrix[9] * this.matrix[15] - this.matrix[11] * this.matrix[13], d11 = this.matrix[10] * this.matrix[15] - this.matrix[11] * this.matrix[14]; // Calculate the determinant return ( d00 * d11 - d01 * d10 + d02 * d09 + d03 * d08 - d04 * d07 + d05 * d06 ); } /** * PRIVATE */ // matrix methods adapted from: // https://developer.mozilla.org/en-US/docs/Web/WebGL/ // gluPerspective // // function _makePerspective(fovy, aspect, znear, zfar){ // const ymax = znear * Math.tan(fovy * Math.PI / 360.0); // const ymin = -ymax; // const xmin = ymin * aspect; // const xmax = ymax * aspect; // return _makeFrustum(xmin, xmax, ymin, ymax, znear, zfar); // } //// //// glFrustum //// //function _makeFrustum(left, right, bottom, top, znear, zfar){ // const X = 2*znear/(right-left); // const Y = 2*znear/(top-bottom); // const A = (right+left)/(right-left); // const B = (top+bottom)/(top-bottom); // const C = -(zfar+znear)/(zfar-znear); // const D = -2*zfar*znear/(zfar-znear); // const frustrumMatrix =[ // X, 0, A, 0, // 0, Y, B, 0, // 0, 0, C, D, // 0, 0, -1, 0 //]; //return frustrumMatrix; // } // function _setMVPMatrices(){ ////an identity matrix ////@TODO use the p5.Matrix class to abstract away our MV matrices and ///other math //const _mvMatrix = //[ // 1.0,0.0,0.0,0.0, // 0.0,1.0,0.0,0.0, // 0.0,0.0,1.0,0.0, // 0.0,0.0,0.0,1.0 //]; } /** * @module Math * @requires constants * @todo see methods below needing further implementation. * future consideration: implement SIMD optimizations * when browser compatibility becomes available * https://developer.mozilla.org/en-US/docs/Web/JavaScript/ * Reference/Global_Objects/SIMD */ // import { MatrixNumjs as Matrix } from './Matrices/MatrixNumjs' function matrix(p5, fn) { /** * A class to describe a matrix * for model and view matrix manipulation in the p5js webgl renderer. * The `Matrix` class represents a mathematical matrix and provides various methods for matrix operations. * * The `Matrix` class represents a mathematical matrix and provides various methods for matrix operations. * This class extends the `MatrixInterface` and includes methods for creating, manipulating, and performing * operations on matrices. It supports both 3x3 and 4x4 matrices, as well as general NxN matrices. * @private * @class p5.Matrix * @param {Array} [mat4] column-major array literal of our 4×4 matrix * @example * // Creating a 3x3 matrix from an array using column major arrangement * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * * // Creating a 4x4 identity matrix * const identityMatrix = new p5.Matrix(4); * * // Adding two matrices * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]); * matrix1.add(matrix2); // matrix1 is now [10, 10, 10, 10, 10, 10, 10, 10, 10] * * // Setting an element in the matrix * matrix.setElement(0, 10); // matrix is now [10, 2, 3, 4, 5, 6, 7, 8, 9] * * // Resetting the matrix to an identity matrix * matrix.reset(); * * // Getting the diagonal elements of the matrix * const diagonal = matrix.diagonal(); // [1, 1, 1] * * // Transposing the matrix * matrix.transpose(); * * // Multiplying two matrices * matrix1.mult(matrix2); * * // Inverting the matrix * matrix.invert(); * * // Scaling the matrix * matrix.scale(2, 2, 2); * * // Rotating the matrix around an axis * matrix.rotate4x4(Math.PI / 4, 1, 0, 0); * * // Applying a perspective transformation * matrix.perspective(Math.PI / 4, 1, 0.1, 100); * * // Applying an orthographic transformation * matrix.ortho(-1, 1, -1, 1, 0.1, 100); * * // Multiplying a vector by the matrix * const vector = new Vector(1, 2, 3); * const result = matrix.multiplyPoint(vector); * * @example * // META:norender * // p5.js script example * function setup() { * * // Create a 4x4 identity matrix * const matrix = new p5.Matrix(4); * console.log("Original p5.Matrix:", matrix.matrix.toString()); // Output: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] * * // Add two matrices * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]); * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]); * matrix1.add(matrix2); * console.log("After Addition:", matrix1.matrix.toString()); // Output: [10, 10, 10, 10, 10, 10, 10, 10, 10] * * // Reset the matrix to an identity matrix * matrix.reset(); * console.log("Reset p5.Matrix:", matrix.matrix.toString()); // [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] * * // Apply a scaling transformation * matrix.scale(2, 2, 2); * console.log("Scaled p5.Matrix:", matrix.matrix.toString()); // [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1] * * // Apply a rotation around the X-axis * matrix.rotate4x4(Math.PI / 4, 1, 0, 0); * console.log("Rotated p5.Matrix (X-axis):", matrix.matrix.toString()); // [2, 0, 0, 0, 0, 1.4142135381698608, 1.4142135381698608, 0, 0, -1.4142135381698608, 1.4142135381698608, 0, 0, 0, 0, 1] * * // Apply a perspective transformation * matrix.perspective(Math.PI / 4, 1, 0.1, 100); * console.log("Perspective p5.Matrix:", matrix.matrix.toString());// [2.4142136573791504, 0, 0, 0, 0, 2.4142136573791504, 0, 0, 0, 0, -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0] * * // Multiply a vector by the matrix * const vector = new p5.Vector(1, 2, 3); * const transformedVector = matrix.multiplyPoint(vector); * console.log("Transformed Vector:", transformedVector.toString()); * } */ p5.Matrix = Matrix; } if (typeof p5 !== 'undefined') { matrix(p5); } class DataArray { constructor(initialLength = 128) { this.length = 0; this.data = new Float32Array(initialLength); this.initialLength = initialLength; } /** * Returns a Float32Array window sized to the exact length of the data */ dataArray() { return this.subArray(0, this.length); } /** * A "soft" clear, which keeps the underlying storage size the same, but * empties the contents of its dataArray() */ clear() { this.length = 0; } /** * Can be used to scale a DataArray back down to fit its contents. */ rescale() { if (this.length < this.data.length / 2) { // Find the power of 2 size that fits the data const targetLength = 1 << Math.ceil(Math.log2(this.length)); const newData = new Float32Array(targetLength); newData.set(this.data.subarray(0, this.length), 0); this.data = newData; } } /** * A full reset, which allocates a new underlying Float32Array at its initial * length */ reset() { this.clear(); this.data = new Float32Array(this.initialLength); } /** * Adds values to the DataArray, expanding its internal storage to * accommodate the new items. */ push(...values) { this.ensureLength(this.length + values.length); this.data.set(values, this.length); this.length += values.length; } /** * Returns a copy of the data from the index `from`, inclusive, to the index * `to`, exclusive */ slice(from, to) { return this.data.slice(from, Math.min(to, this.length)); } /** * Returns a mutable Float32Array window from the index `from`, inclusive, to * the index `to`, exclusive */ subArray(from, to) { return this.data.subarray(from, Math.min(to, this.length)); } /** * Expand capacity of the internal storage until it can fit a target size */ ensureLength(target) { while (this.data.length < target) { const newData = new Float32Array(this.data.length * 2); newData.set(this.data, 0); this.data = newData; } } } function dataArray(p5, fn){ /** * An internal class to store data that will be sent to a p5.RenderBuffer. * Those need to eventually go into a Float32Array, so this class provides a * variable-length array container backed by a Float32Array so that it can be * sent to the GPU without allocating a new array each frame. * * Like a C++ vector, its fixed-length Float32Array backing its contents will * double in size when it goes over its capacity. * * @example * // Initialize storage with a capacity of 4 * const storage = new DataArray(4); * console.log(storage.data.length); // 4 * console.log(storage.length); // 0 * console.log(storage.dataArray()); // Empty Float32Array * * storage.push(1, 2, 3, 4, 5, 6); * console.log(storage.data.length); // 8 * console.log(storage.length); // 6 * console.log(storage.dataArray()); // Float32Array{1, 2, 3, 4, 5, 6} */ p5.DataArray = DataArray; } if(typeof p5 !== 'undefined'){ dataArray(p5); } /** * @module Shape * @submodule 3D Primitives * @for p5 * @requires core * @requires p5.Geometry */ class Geometry { constructor(detailX, detailY, callback, renderer) { this.renderer = renderer; this.vertices = []; this.boundingBoxCache = null; //an array containing every vertex for stroke drawing this.lineVertices = new DataArray(); // The tangents going into or out of a vertex on a line. Along a straight // line segment, both should be equal. At an endpoint, one or the other // will not exist and will be all 0. In joins between line segments, they // may be different, as they will be the tangents on either side of the join. this.lineTangentsIn = new DataArray(); this.lineTangentsOut = new DataArray(); // When drawing lines with thickness, entries in this buffer represent which // side of the centerline the vertex will be placed. The sign of the number // will represent the side of the centerline, and the absolute value will be // used as an enum to determine which part of the cap or join each vertex // represents. See the doc comments for _addCap and _addJoin for diagrams. this.lineSides = new DataArray(); this.vertexNormals = []; this.faces = []; this.uvs = []; // a 2D array containing edge connectivity pattern for create line vertices //based on faces for most objects; this.edges = []; this.vertexColors = []; // One color per vertex representing the stroke color at that vertex this.vertexStrokeColors = []; this.userVertexProperties = {}; // One color per line vertex, generated automatically based on // vertexStrokeColors in _edgesToVertices() this.lineVertexColors = new DataArray(); this.detailX = detailX !== undefined ? detailX : 1; this.detailY = detailY !== undefined ? detailY : 1; this.dirtyFlags = {}; this._hasFillTransparency = undefined; this._hasStrokeTransparency = undefined; this.gid = `_p5_Geometry_${Geometry.nextId}`; Geometry.nextId++; if (callback instanceof Function) { callback.call(this); } } /** * Calculates the position and size of the smallest box that contains the geometry. * * A bounding box is the smallest rectangular prism that contains the entire * geometry. It's defined by the box's minimum and maximum coordinates along * each axis, as well as the size (length) and offset (center). * * Calling `myGeometry.calculateBoundingBox()` returns an object with four * properties that describe the bounding box: * * ```js * // Get myGeometry's bounding box. * let bbox = myGeometry.calculateBoundingBox(); * * // Print the bounding box to the console. * console.log(bbox); * * // { * // // The minimum coordinate along each axis. * // min: { x: -1, y: -2, z: -3 }, * // * // // The maximum coordinate along each axis. * // max: { x: 1, y: 2, z: 3}, * // * // // The size (length) along each axis. * // size: { x: 2, y: 4, z: 6}, * // * // // The offset (center) along each axis. * // offset: { x: 0, y: 0, z: 0} * // } * ``` * * @returns {Object} bounding box of the geometry. * * @example * // Click and drag the mouse to view the scene from different angles. * * let particles; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a new p5.Geometry object with random spheres. * particles = buildGeometry(createParticles); * * describe('Ten white spheres placed randomly against a gray background. A box encloses the spheres.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the particles. * noStroke(); * fill(255); * * // Draw the particles. * model(particles); * * // Calculate the bounding box. * let bbox = particles.calculateBoundingBox(); * * // Translate to the bounding box's center. * translate(bbox.offset.x, bbox.offset.y, bbox.offset.z); * * // Style the bounding box. * stroke(255); * noFill(); * * // Draw the bounding box. * box(bbox.size.x, bbox.size.y, bbox.size.z); * } * * function createParticles() { * for (let i = 0; i < 10; i += 1) { * // Calculate random coordinates. * let x = randomGaussian(0, 15); * let y = randomGaussian(0, 15); * let z = randomGaussian(0, 15); * * push(); * // Translate to the particle's coordinates. * translate(x, y, z); * // Draw the particle. * sphere(3); * pop(); * } * } */ calculateBoundingBox() { if (this.boundingBoxCache) { return this.boundingBoxCache; // Return cached result if available } let minVertex = new Vector( Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE); let maxVertex = new Vector( Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE); for (let i = 0; i < this.vertices.length; i++) { let vertex = this.vertices[i]; minVertex.x = Math.min(minVertex.x, vertex.x); minVertex.y = Math.min(minVertex.y, vertex.y); minVertex.z = Math.min(minVertex.z, vertex.z); maxVertex.x = Math.max(maxVertex.x, vertex.x); maxVertex.y = Math.max(maxVertex.y, vertex.y); maxVertex.z = Math.max(maxVertex.z, vertex.z); } // Calculate size and offset properties let size = new Vector(maxVertex.x - minVertex.x, maxVertex.y - minVertex.y, maxVertex.z - minVertex.z); let offset = new Vector((minVertex.x + maxVertex.x) / 2, (minVertex.y + maxVertex.y) / 2, (minVertex.z + maxVertex.z) / 2); // Cache the result for future access this.boundingBoxCache = { min: minVertex, max: maxVertex, size: size, offset: offset }; return this.boundingBoxCache; } reset() { // Notify renderer that geometry is being reset (for buffer cleanup) this.renderer?.onReset?.(this); this._hasFillTransparency = undefined; this._hasStrokeTransparency = undefined; this.lineVertices.clear(); this.lineTangentsIn.clear(); this.lineTangentsOut.clear(); this.lineSides.clear(); this.vertices.length = 0; this.edges.length = 0; this.vertexColors.length = 0; this.vertexStrokeColors.length = 0; this.lineVertexColors.clear(); this.vertexNormals.length = 0; this.uvs.length = 0; for (const propName in this.userVertexProperties){ this.userVertexProperties[propName].delete(); } this.userVertexProperties = {}; this.dirtyFlags = {}; } hasFillTransparency() { if (this._hasFillTransparency === undefined) { this._hasFillTransparency = false; for (let i = 0; i < this.vertexColors.length; i += 4) { if (this.vertexColors[i + 3] < 1) { this._hasFillTransparency = true; break; } } } return this._hasFillTransparency; } hasStrokeTransparency() { if (this._hasStrokeTransparency === undefined) { this._hasStrokeTransparency = false; for (let i = 0; i < this.lineVertexColors.length; i += 4) { if (this.lineVertexColors[i + 3] < 1) { this._hasStrokeTransparency = true; break; } } } return this._hasStrokeTransparency; } /** * Removes the geometry’s internal colors. * * `p5.Geometry` objects can be created with "internal colors" assigned to * vertices or the entire shape. When a geometry has internal colors, * fill() has no effect. Calling * `myGeometry.clearColors()` allows the * fill() function to apply color to the geometry. * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Geometry object. * // Set its internal color to red. * let myGeometry = buildGeometry(function() { * fill(255, 0, 0); * plane(20); * }); * * // Style the shape. * noStroke(); * * // Draw the p5.Geometry object (center). * model(myGeometry); * * // Translate the origin to the bottom-right. * translate(25, 25, 0); * * // Try to fill the geometry with green. * fill(0, 255, 0); * * // Draw the geometry again (bottom-right). * model(myGeometry); * * // Clear the geometry's colors. * myGeometry.clearColors(); * * // Fill the geometry with blue. * fill(0, 0, 255); * * // Translate the origin up. * translate(0, -50, 0); * * // Draw the geometry again (top-right). * model(myGeometry); * * describe( * 'Three squares drawn against a gray background. Red squares are at the center and the bottom-right. A blue square is at the top-right.' * ); * } */ clearColors() { this.vertexColors = []; return this; } /** * The `saveObj()` function exports `p5.Geometry` objects as * 3D models in the Wavefront .obj file format. * This way, you can use the 3D shapes you create in p5.js in other software * for rendering, animation, 3D printing, or more. * * The exported .obj file will include the faces and vertices of the `p5.Geometry`, * as well as its texture coordinates and normals, if it has them. * * @method saveObj * @param {String} [fileName='model.obj'] The name of the file to save the model as. * If not specified, the default file name will be 'model.obj'. * @example * let myModel; * let saveBtn; * function setup() { * createCanvas(200, 200, WEBGL); * myModel = buildGeometry(function()) { * for (let i = 0; i < 5; i++) { * push(); * translate( * random(-75, 75), * random(-75, 75), * random(-75, 75) * ); * sphere(random(5, 50)); * pop(); * } * }); * * saveBtn = createButton('Save .obj'); * saveBtn.mousePressed(() => myModel.saveObj()); * * describe('A few spheres rotating in space'); * } * * function draw() { * background(0); * noStroke(); * lights(); * rotateX(millis() * 0.001); * rotateY(millis() * 0.002); * model(myModel); * } */ saveObj(fileName = 'model.obj') { let objStr= ''; // Vertices this.vertices.forEach(v => { objStr += `v ${v.x} ${v.y} ${v.z}\n`; }); // Texture Coordinates (UVs) if (this.uvs && this.uvs.length > 0) { for (let i = 0; i < this.uvs.length; i += 2) { objStr += `vt ${this.uvs[i]} ${this.uvs[i + 1]}\n`; } } // Vertex Normals if (this.vertexNormals && this.vertexNormals.length > 0) { this.vertexNormals.forEach(n => { objStr += `vn ${n.x} ${n.y} ${n.z}\n`; }); } // Faces, obj vertex indices begin with 1 and not 0 // texture coordinate (uvs) and vertexNormal indices // are indicated with trailing ints vertex/normal/uv // ex 1/1/1 or 2//2 for vertices without uvs this.faces.forEach(face => { let faceStr = 'f'; face.forEach(index =>{ faceStr += ' '; faceStr += index + 1; if (this.vertexNormals.length > 0 || this.uvs.length > 0) { faceStr += '/'; if (this.uvs.length > 0) { faceStr += index + 1; } faceStr += '/'; if (this.vertexNormals.length > 0) { faceStr += index + 1; } } }); objStr += faceStr + '\n'; }); const blob = new Blob([objStr], { type: 'text/plain' }); downloadFile(blob, fileName , 'obj'); } /** * The `saveStl()` function exports `p5.Geometry` objects as * 3D models in the STL stereolithography file format. * This way, you can use the 3D shapes you create in p5.js in other software * for rendering, animation, 3D printing, or more. * * The exported .stl file will include the faces, vertices, and normals of the `p5.Geometry`. * * By default, this method saves a text-based .stl file. Alternatively, you can save a more compact * but less human-readable binary .stl file by passing `{ binary: true }` as a second parameter. * * @method saveStl * @param {String} [fileName='model.stl'] The name of the file to save the model as. * If not specified, the default file name will be 'model.stl'. * @param {Object} [options] Optional settings. * @param {Boolean} [options.binary=false] Whether or not a binary .stl file is saved. * @example * let myModel; * let saveBtn1; * let saveBtn2; * function setup() { * createCanvas(200, 200, WEBGL); * myModel = buildGeometry(function() { * for (let i = 0; i < 5; i++) { * push(); * translate( * random(-75, 75), * random(-75, 75), * random(-75, 75) * ); * sphere(random(5, 50)); * pop(); * } * }); * * saveBtn1 = createButton('Save .stl'); * saveBtn1.mousePressed(function() { * myModel.saveStl(); * }); * saveBtn2 = createButton('Save binary .stl'); * saveBtn2.mousePressed(function() { * myModel.saveStl('model.stl', { binary: true }); * }); * * describe('A few spheres rotating in space'); * } * * function draw() { * background(0); * noStroke(); * lights(); * rotateX(millis() * 0.001); * rotateY(millis() * 0.002); * model(myModel); * } */ saveStl(fileName = 'model.stl', { binary = false } = {}){ let modelOutput; let name = fileName.substring(0, fileName.lastIndexOf('.')); let faceNormals = []; for (let f of this.faces) { const U = Vector.sub(this.vertices[f[1]], this.vertices[f[0]]); const V = Vector.sub(this.vertices[f[2]], this.vertices[f[0]]); const nx = U.y * V.z - U.z * V.y; const ny = U.z * V.x - U.x * V.z; const nz = U.x * V.y - U.y * V.x; faceNormals.push(new Vector(nx, ny, nz).normalize()); } if (binary) { let offset = 80; const bufferLength = this.faces.length * 2 + this.faces.length * 3 * 4 * 4 + 80 + 4; const arrayBuffer = new ArrayBuffer(bufferLength); modelOutput = new DataView(arrayBuffer); modelOutput.setUint32(offset, this.faces.length, true); offset += 4; for (const [key, f] of Object.entries(this.faces)) { const norm = faceNormals[key]; modelOutput.setFloat32(offset, norm.x, true); offset += 4; modelOutput.setFloat32(offset, norm.y, true); offset += 4; modelOutput.setFloat32(offset, norm.z, true); offset += 4; for (let vertexIndex of f) { const vert = this.vertices[vertexIndex]; modelOutput.setFloat32(offset, vert.x, true); offset += 4; modelOutput.setFloat32(offset, vert.y, true); offset += 4; modelOutput.setFloat32(offset, vert.z, true); offset += 4; } modelOutput.setUint16(offset, 0, true); offset += 2; } } else { modelOutput = 'solid ' + name + '\n'; for (const [key, f] of Object.entries(this.faces)) { const norm = faceNormals[key]; modelOutput += ' facet norm ' + norm.x + ' ' + norm.y + ' ' + norm.z + '\n'; modelOutput += ' outer loop' + '\n'; for (let vertexIndex of f) { const vert = this.vertices[vertexIndex]; modelOutput += ' vertex ' + vert.x + ' ' + vert.y + ' ' + vert.z + '\n'; } modelOutput += ' endloop' + '\n'; modelOutput += ' endfacet' + '\n'; } modelOutput += 'endsolid ' + name + '\n'; } const blob = new Blob([modelOutput], { type: 'text/plain' }); downloadFile(blob, fileName, 'stl'); } /** * Flips the geometry’s texture u-coordinates. * * In order for texture() to work, the geometry * needs a way to map the points on its surface to the pixels in a rectangular * image that's used as a texture. The geometry's vertex at coordinates * `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`. * * The myGeometry.uvs array stores the * `(u, v)` coordinates for each vertex in the order it was added to the * geometry. Calling `myGeometry.flipU()` flips a geometry's u-coordinates * so that the texture appears mirrored horizontally. * * For example, a plane's four vertices are added clockwise starting from the * top-left corner. Here's how calling `myGeometry.flipU()` would change a * plane's texture coordinates: * * ```js * // Print the original texture coordinates. * // Output: [0, 0, 1, 0, 0, 1, 1, 1] * console.log(myGeometry.uvs); * * // Flip the u-coordinates. * myGeometry.flipU(); * * // Print the flipped texture coordinates. * // Output: [1, 0, 0, 0, 1, 1, 0, 1] * console.log(myGeometry.uvs); * * // Notice the swaps: * // Top vertices: [0, 0, 1, 0] --> [1, 0, 0, 0] * // Bottom vertices: [0, 1, 1, 1] --> [1, 1, 0, 1] * ``` * * @for p5.Geometry * * @example * let img; * * async function setup() { * img = await loadImage('assets/laDefense.jpg'); * createCanvas(100, 100, WEBGL); * * background(200); * * // Create p5.Geometry objects. * let geom1 = buildGeometry(createShape); * let geom2 = buildGeometry(createShape); * * // Flip geom2's U texture coordinates. * geom2.flipU(); * * // Left (original). * push(); * translate(-25, 0, 0); * texture(img); * noStroke(); * model(geom1); * pop(); * * // Right (flipped). * push(); * translate(25, 0, 0); * texture(img); * noStroke(); * model(geom2); * pop(); * * describe( * 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.' * ); * } * * function createShape() { * plane(40); * } */ flipU() { this.uvs = this.uvs.flat().map((val, index) => { if (index % 2 === 0) { return 1 - val; } else { return val; } }); } /** * Flips the geometry’s texture v-coordinates. * * In order for texture() to work, the geometry * needs a way to map the points on its surface to the pixels in a rectangular * image that's used as a texture. The geometry's vertex at coordinates * `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`. * * The myGeometry.uvs array stores the * `(u, v)` coordinates for each vertex in the order it was added to the * geometry. Calling `myGeometry.flipV()` flips a geometry's v-coordinates * so that the texture appears mirrored vertically. * * For example, a plane's four vertices are added clockwise starting from the * top-left corner. Here's how calling `myGeometry.flipV()` would change a * plane's texture coordinates: * * ```js * // Print the original texture coordinates. * // Output: [0, 0, 1, 0, 0, 1, 1, 1] * console.log(myGeometry.uvs); * * // Flip the v-coordinates. * myGeometry.flipV(); * * // Print the flipped texture coordinates. * // Output: [0, 1, 1, 1, 0, 0, 1, 0] * console.log(myGeometry.uvs); * * // Notice the swaps: * // Left vertices: [0, 0] <--> [1, 0] * // Right vertices: [1, 0] <--> [1, 1] * ``` * * @method flipV * @for p5.Geometry * * @example * let img; * * async function setup() { * img = await loadImage('assets/laDefense.jpg'); * createCanvas(100, 100, WEBGL); * * background(200); * * // Create p5.Geometry objects. * let geom1 = buildGeometry(createShape); * let geom2 = buildGeometry(createShape); * * // Flip geom2's V texture coordinates. * geom2.flipV(); * * // Left (original). * push(); * translate(-25, 0, 0); * texture(img); * noStroke(); * model(geom1); * pop(); * * // Right (flipped). * push(); * translate(25, 0, 0); * texture(img); * noStroke(); * model(geom2); * pop(); * * describe( * 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.' * ); * } * * function createShape() { * plane(40); * } */ flipV() { this.uvs = this.uvs.flat().map((val, index) => { if (index % 2 === 0) { return val; } else { return 1 - val; } }); } /** * Computes the geometry's faces using its vertices. * * All 3D shapes are made by connecting sets of points called *vertices*. A * geometry's surface is formed by connecting vertices to form triangles that * are stitched together. Each triangular patch on the geometry's surface is * called a *face*. `myGeometry.computeFaces()` performs the math needed to * define each face based on the distances between vertices. * * The geometry's vertices are stored as p5.Vector * objects in the myGeometry.vertices * array. The geometry's first vertex is the * p5.Vector object at `myGeometry.vertices[0]`, * its second vertex is `myGeometry.vertices[1]`, its third vertex is * `myGeometry.vertices[2]`, and so on. * * Calling `myGeometry.computeFaces()` fills the * myGeometry.faces array with three-element * arrays that list the vertices that form each face. For example, a geometry * made from a rectangle has two faces because a rectangle is made by joining * two triangles. myGeometry.faces for a * rectangle would be the two-dimensional array * `[[0, 1, 2], [2, 1, 3]]`. The first face, `myGeometry.faces[0]`, is the * array `[0, 1, 2]` because it's formed by connecting * `myGeometry.vertices[0]`, `myGeometry.vertices[1]`,and * `myGeometry.vertices[2]`. The second face, `myGeometry.faces[1]`, is the * array `[2, 1, 3]` because it's formed by connecting * `myGeometry.vertices[2]`, `myGeometry.vertices[1]`, and * `myGeometry.vertices[3]`. * * Note: `myGeometry.computeFaces()` only works when geometries have four or more vertices. * * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = new p5.Geometry(); * * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(0, 40, 0); * let v3 = createVector(40, 0, 0); * * // Add the vertices to myGeometry's vertices array. * myGeometry.vertices.push(v0, v1, v2, v3); * * // Compute myGeometry's faces array. * myGeometry.computeFaces(); * * describe('A red square drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the shape. * noStroke(); * fill(255, 0, 0); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object using a callback function. * myGeometry = new p5.Geometry(1, 1, createShape); * * describe('A red square drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the shape. * noStroke(); * fill(255, 0, 0); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * function createShape() { * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(0, 40, 0); * let v3 = createVector(40, 0, 0); * * // Add the vertices to the p5.Geometry object's vertices array. * this.vertices.push(v0, v1, v2, v3); * * // Compute the faces array. * this.computeFaces(); * } */ computeFaces() { this.faces.length = 0; const sliceCount = this.detailX + 1; let a, b, c, d; for (let i = 0; i < this.detailY; i++) { for (let j = 0; j < this.detailX; j++) { a = i * sliceCount + j; // + offset; b = i * sliceCount + j + 1; // + offset; c = (i + 1) * sliceCount + j + 1; // + offset; d = (i + 1) * sliceCount + j; // + offset; this.faces.push([a, b, d]); this.faces.push([d, b, c]); } } return this; } _getFaceNormal(faceId) { //This assumes that vA->vB->vC is a counter-clockwise ordering const face = this.faces[faceId]; const vA = this.vertices[face[0]]; const vB = this.vertices[face[1]]; const vC = this.vertices[face[2]]; const ab = Vector.sub(vB, vA); const ac = Vector.sub(vC, vA); const n = Vector.cross(ab, ac); const ln = Vector.mag(n); let sinAlpha = ln / (Vector.mag(ab) * Vector.mag(ac)); if (sinAlpha === 0 || isNaN(sinAlpha)) { console.warn( 'p5.Geometry.prototype._getFaceNormal:', 'face has colinear sides or a repeated vertex' ); return n; } if (sinAlpha > 1) sinAlpha = 1; // handle float rounding error return n.mult(Math.asin(sinAlpha) / ln); } /** * Calculates the normal vector for each vertex on the geometry. * * All 3D shapes are made by connecting sets of points called *vertices*. A * geometry's surface is formed by connecting vertices to create triangles * that are stitched together. Each triangular patch on the geometry's * surface is called a *face*. `myGeometry.computeNormals()` performs the * math needed to orient each face. Orientation is important for lighting * and other effects. * * A face's orientation is defined by its *normal vector* which points out * of the face and is normal (perpendicular) to the surface. Calling * `myGeometry.computeNormals()` first calculates each face's normal vector. * Then it calculates the normal vector for each vertex by averaging the * normal vectors of the faces surrounding the vertex. The vertex normals * are stored as p5.Vector objects in the * myGeometry.vertexNormals array. * * The first parameter, `shadingType`, is optional. Passing the constant * `FLAT`, as in `myGeometry.computeNormals(FLAT)`, provides neighboring * faces with their own copies of the vertices they share. Surfaces appear * tiled with flat shading. Passing the constant `SMOOTH`, as in * `myGeometry.computeNormals(SMOOTH)`, makes neighboring faces reuse their * shared vertices. Surfaces appear smoother with smooth shading. By * default, `shadingType` is `FLAT`. * * The second parameter, `options`, is also optional. If an object with a * `roundToPrecision` property is passed, as in * `myGeometry.computeNormals(SMOOTH, { roundToPrecision: 5 })`, it sets the * number of decimal places to use for calculations. By default, * `roundToPrecision` uses 3 decimal places. * * @param {(FLAT|SMOOTH)} [shadingType=FLAT] shading type. either FLAT or SMOOTH. Defaults to `FLAT`. * @param {Object} [options] shading options. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = buildGeometry(function() { * torus(); * }); * * // Compute the vertex normals. * myGeometry.computeNormals(); * * describe( * "A white torus drawn on a dark gray background. Red lines extend outward from the torus' vertices." * ); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateX(1); * * // Style the helix. * stroke(0); * * // Display the helix. * model(myGeometry); * * // Style the normal vectors. * stroke(255, 0, 0); * * // Iterate over the vertices and vertexNormals arrays. * for (let i = 0; i < myGeometry.vertices.length; i += 1) { * * // Get the vertex p5.Vector object. * let v = myGeometry.vertices[i]; * * // Get the vertex normal p5.Vector object. * let n = myGeometry.vertexNormals[i]; * * // Calculate a point along the vertex normal. * let p = p5.Vector.mult(n, 5); * * // Draw the vertex normal as a red line. * push(); * translate(v); * line(0, 0, 0, p.x, p.y, p.z); * pop(); * } * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object using a callback function. * myGeometry = new p5.Geometry(); * * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(0, 40, 0); * let v3 = createVector(40, 0, 0); * * // Add the vertices to the p5.Geometry object's vertices array. * myGeometry.vertices.push(v0, v1, v2, v3); * * // Compute the faces array. * myGeometry.computeFaces(); * * // Compute the surface normals. * myGeometry.computeNormals(); * * describe('A red square drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a white point light. * pointLight(255, 255, 255, 0, 0, 10); * * // Style the p5.Geometry object. * noStroke(); * fill(255, 0, 0); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = buildGeometry(createShape); * * // Compute normals using default (FLAT) shading. * myGeometry.computeNormals(FLAT); * * describe('A white, helical structure drawn on a dark gray background. Its faces appear faceted.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateX(1); * * // Style the helix. * noStroke(); * * // Display the helix. * model(myGeometry); * } * * function createShape() { * // Create a helical shape. * beginShape(); * for (let i = 0; i < TWO_PI * 3; i += 0.5) { * let x = 30 * cos(i); * let y = 30 * sin(i); * let z = map(i, 0, TWO_PI * 3, -40, 40); * vertex(x, y, z); * } * endShape(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = buildGeometry(createShape); * * // Compute normals using smooth shading. * myGeometry.computeNormals(SMOOTH); * * describe('A white, helical structure drawn on a dark gray background.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateX(1); * * // Style the helix. * noStroke(); * * // Display the helix. * model(myGeometry); * } * * function createShape() { * // Create a helical shape. * beginShape(); * for (let i = 0; i < TWO_PI * 3; i += 0.5) { * let x = 30 * cos(i); * let y = 30 * sin(i); * let z = map(i, 0, TWO_PI * 3, -40, 40); * vertex(x, y, z); * } * endShape(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = buildGeometry(createShape); * * // Create an options object. * let options = { roundToPrecision: 5 }; * * // Compute normals using smooth shading. * myGeometry.computeNormals(SMOOTH, options); * * describe('A white, helical structure drawn on a dark gray background.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateX(1); * * // Style the helix. * noStroke(); * * // Display the helix. * model(myGeometry); * } * * function createShape() { * // Create a helical shape. * beginShape(); * for (let i = 0; i < TWO_PI * 3; i += 0.5) { * let x = 30 * cos(i); * let y = 30 * sin(i); * let z = map(i, 0, TWO_PI * 3, -40, 40); * vertex(x, y, z); * } * endShape(); * } */ computeNormals(shadingType = FLAT, { roundToPrecision = 3 } = {}) { const vertexNormals = this.vertexNormals; let vertices = this.vertices; const faces = this.faces; let iv; if (shadingType === SMOOTH) { const vertexIndices = {}; const uniqueVertices = []; const power = Math.pow(10, roundToPrecision); const rounded = val => Math.round(val * power) / power; const getKey = vert => `${rounded(vert.x)},${rounded(vert.y)},${rounded(vert.z)}`; // loop through each vertex and add uniqueVertices for (let i = 0; i < vertices.length; i++) { const vertex = vertices[i]; const key = getKey(vertex); if (vertexIndices[key] === undefined) { vertexIndices[key] = uniqueVertices.length; uniqueVertices.push(vertex); } } // update face indices to use the deduplicated vertex indices faces.forEach(face => { for (let fv = 0; fv < 3; ++fv) { const originalVertexIndex = face[fv]; const originalVertex = vertices[originalVertexIndex]; const key = getKey(originalVertex); face[fv] = vertexIndices[key]; } }); // update edge indices to use the deduplicated vertex indices this.edges.forEach(edge => { for (let ev = 0; ev < 2; ++ev) { const originalVertexIndex = edge[ev]; const originalVertex = vertices[originalVertexIndex]; const key = getKey(originalVertex); edge[ev] = vertexIndices[key]; } }); // update the deduplicated vertices this.vertices = vertices = uniqueVertices; } // initialize the vertexNormals array with empty vectors vertexNormals.length = 0; for (iv = 0; iv < vertices.length; ++iv) { vertexNormals.push(new Vector()); } // loop through all the faces adding its normal to the normal // of each of its vertices faces.forEach((face, f) => { const faceNormal = this._getFaceNormal(f); // all three vertices get the normal added for (let fv = 0; fv < 3; ++fv) { const vertexIndex = face[fv]; vertexNormals[vertexIndex].add(faceNormal); } }); // normalize the normals for (iv = 0; iv < vertices.length; ++iv) { vertexNormals[iv].normalize(); } return this; } /** * Averages the vertex normals. Used in curved * surfaces * @private * @chainable */ averageNormals() { for (let i = 0; i <= this.detailY; i++) { const offset = this.detailX + 1; let temp = Vector.add( this.vertexNormals[i * offset], this.vertexNormals[i * offset + this.detailX] ); temp = Vector.div(temp, 2); this.vertexNormals[i * offset] = temp; this.vertexNormals[i * offset + this.detailX] = temp; } return this; } /** * Averages pole normals. Used in spherical primitives * @private * @chainable */ averagePoleNormals() { //average the north pole let sum = new Vector(0, 0, 0); for (let i = 0; i < this.detailX; i++) { sum.add(this.vertexNormals[i]); } sum = Vector.div(sum, this.detailX); for (let i = 0; i < this.detailX; i++) { this.vertexNormals[i] = sum; } //average the south pole sum = new Vector(0, 0, 0); for ( let i = this.vertices.length - 1; i > this.vertices.length - 1 - this.detailX; i-- ) { sum.add(this.vertexNormals[i]); } sum = Vector.div(sum, this.detailX); for ( let i = this.vertices.length - 1; i > this.vertices.length - 1 - this.detailX; i-- ) { this.vertexNormals[i] = sum; } return this; } /** * Create a 2D array for establishing stroke connections * @private * @chainable */ _makeTriangleEdges() { this.edges.length = 0; for (let j = 0; j < this.faces.length; j++) { this.edges.push([this.faces[j][0], this.faces[j][1]]); this.edges.push([this.faces[j][1], this.faces[j][2]]); this.edges.push([this.faces[j][2], this.faces[j][0]]); } return this; } /** * @example * let tetrahedron; * function setup() { * createCanvas(200, 200, WEBGL); * describe('A rotating tetrahedron'); * * tetrahedron = new p5.Geometry(); * * // Give each geometry a unique gid * tetrahedron.gid = 'tetrahedron'; * * // Add four points of the tetrahedron * * let radius = 50; * // A 2D triangle: * tetrahedron.vertices.push(createVector(radius, 0, 0)); * tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI / 3)); * tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI * 2 / 3)); * // Add a tip in the z axis: * tetrahedron.vertices.push(createVector(0, 0, radius)); * * // Create the four faces by connecting the sets of three points * tetrahedron.faces.push([0, 1, 2]); * tetrahedron.faces.push([0, 1, 3]); * tetrahedron.faces.push([0, 2, 3]); * tetrahedron.faces.push([1, 2, 3]); * tetrahedron.makeEdgesFromFaces(); * } * * function draw() { * background(200); * strokeWeight(2); * orbitControl(); * rotateY(millis() * 0.001); * model(tetrahedron); * } */ makeEdgesFromFaces() { this._makeTriangleEdges(); } /** * Converts each line segment into the vertices and vertex attributes needed * to turn the line into a polygon on screen. This will include: * - Two triangles line segment to create a rectangle * - Two triangles per endpoint to create a stroke cap rectangle. A fragment * shader is responsible for displaying the appropriate cap style within * that rectangle. * - Four triangles per join between adjacent line segments, creating a quad on * either side of the join, perpendicular to the lines. A vertex shader will * discard the quad in the "elbow" of the join, and a fragment shader will * display the appropriate join style within the remaining quad. * * @private * @chainable */ _edgesToVertices() { this.lineVertices.clear(); this.lineTangentsIn.clear(); this.lineTangentsOut.clear(); this.lineSides.clear(); const potentialCaps = new Map(); const connected = new Set(); let lastValidDir; for (let i = 0; i < this.edges.length; i++) { const prevEdge = this.edges[i - 1]; const currEdge = this.edges[i]; const isPoint = currEdge[0] === currEdge[1]; const begin = this.vertices[currEdge[0]]; const end = this.vertices[currEdge[1]]; const prevColor = (this.vertexStrokeColors.length > 0 && prevEdge) ? this.vertexStrokeColors.slice( prevEdge[1] * 4, (prevEdge[1] + 1) * 4 ) : [0, 0, 0, 0]; const fromColor = this.vertexStrokeColors.length > 0 ? this.vertexStrokeColors.slice( currEdge[0] * 4, (currEdge[0] + 1) * 4 ) : [0, 0, 0, 0]; const toColor = this.vertexStrokeColors.length > 0 ? this.vertexStrokeColors.slice( currEdge[1] * 4, (currEdge[1] + 1) * 4 ) : [0, 0, 0, 0]; const dir = isPoint ? new Vector(0, 1, 0) : end .copy() .sub(begin) .normalize(); const dirOK = dir.magSq() > 0; if (dirOK) { this._addSegment(begin, end, fromColor, toColor, dir); } if (!this.renderer?._simpleLines) { if (i > 0 && prevEdge[1] === currEdge[0]) { if (!connected.has(currEdge[0])) { connected.add(currEdge[0]); potentialCaps.delete(currEdge[0]); // Add a join if this segment shares a vertex with the previous. Skip // actually adding join vertices if either the previous segment or this // one has a length of 0. // // Don't add a join if the tangents point in the same direction, which // would mean the edges line up exactly, and there is no need for a join. if (lastValidDir && dirOK && dir.dot(lastValidDir) < 1 - 1e-8) { this._addJoin(begin, lastValidDir, dir, fromColor); } } } else if (isPoint) { this._addCap(begin, dir.copy().mult(-1), fromColor); this._addCap(begin, dir, fromColor); } else { // Start a new line if (dirOK && !connected.has(currEdge[0])) { const existingCap = potentialCaps.get(currEdge[0]); if (existingCap) { this._addJoin( begin, existingCap.dir, dir, fromColor ); potentialCaps.delete(currEdge[0]); connected.add(currEdge[0]); } else { potentialCaps.set(currEdge[0], { point: begin, dir: dir.copy().mult(-1), color: fromColor }); } } if (!isPoint && lastValidDir && !connected.has(prevEdge[1])) { const existingCap = potentialCaps.get(prevEdge[1]); if (existingCap) { this._addJoin( this.vertices[prevEdge[1]], lastValidDir, existingCap.dir.copy().mult(-1), prevColor ); potentialCaps.delete(prevEdge[1]); connected.add(prevEdge[1]); } else { // Close off the last segment with a cap potentialCaps.set(prevEdge[1], { point: this.vertices[prevEdge[1]], dir: lastValidDir, color: prevColor }); } lastValidDir = undefined; } } if (i === this.edges.length - 1 && !connected.has(currEdge[1])) { const existingCap = potentialCaps.get(currEdge[1]); if (existingCap) { this._addJoin( end, dir, existingCap.dir.copy().mult(-1), toColor ); potentialCaps.delete(currEdge[1]); connected.add(currEdge[1]); } else { potentialCaps.set(currEdge[1], { point: end, dir, color: toColor }); } } if (dirOK) { lastValidDir = dir; } } } for (const { point, dir, color } of potentialCaps.values()) { this._addCap(point, dir, color); } return this; } /** * Adds the vertices and vertex attributes for two triangles making a rectangle * for a straight line segment. A vertex shader is responsible for picking * proper coordinates on the screen given the centerline positions, the tangent, * and the side of the centerline each vertex belongs to. Sides follow the * following scheme: * * -1 -1 * o-------------o * | | * o-------------o * 1 1 * * @private * @chainable */ _addSegment( begin, end, fromColor, toColor, dir ) { const a = begin.array(); const b = end.array(); const dirArr = dir.array(); this.lineSides.push(1, 1, -1, 1, -1, -1); for (const tangents of [this.lineTangentsIn, this.lineTangentsOut]) { for (let i = 0; i < 6; i++) { tangents.push(...dirArr); } } this.lineVertices.push(...a, ...b, ...a, ...b, ...b, ...a); if (!this.renderer?._simpleLines) { this.lineVertexColors.push( ...fromColor, ...toColor, ...fromColor, ...toColor, ...toColor, ...fromColor ); } return this; } /** * Adds the vertices and vertex attributes for two triangles representing the * stroke cap of a line. A fragment shader is responsible for displaying the * appropriate cap style within the rectangle they make. * * The lineSides buffer will include the following values for the points on * the cap rectangle: * * -1 -2 * -----------o---o * | | * -----------o---o * 1 2 * @private * @chainable */ _addCap(point, tangent, color) { const ptArray = point.array(); const tanInArray = tangent.array(); const tanOutArray = [0, 0, 0]; for (let i = 0; i < 6; i++) { this.lineVertices.push(...ptArray); this.lineTangentsIn.push(...tanInArray); this.lineTangentsOut.push(...tanOutArray); this.lineVertexColors.push(...color); } this.lineSides.push(-1, 2, -2, 1, 2, -1); return this; } /** * Adds the vertices and vertex attributes for four triangles representing a * join between two adjacent line segments. This creates a quad on either side * of the shared vertex of the two line segments, with each quad perpendicular * to the lines. A vertex shader will discard all but the quad in the "elbow" of * the join, and a fragment shader will display the appropriate join style * within the remaining quad. * * The lineSides buffer will include the following values for the points on * the join rectangles: * * -1 -2 * -------------o----o * | | * 1 o----o----o -3 * | | 0 | * --------o----o | * 2| 3 | * | | * | | * @private * @chainable */ _addJoin( point, fromTangent, toTangent, color ) { const ptArray = point.array(); const tanInArray = fromTangent.array(); const tanOutArray = toTangent.array(); for (let i = 0; i < 12; i++) { this.lineVertices.push(...ptArray); this.lineTangentsIn.push(...tanInArray); this.lineTangentsOut.push(...tanOutArray); this.lineVertexColors.push(...color); } this.lineSides.push(-1, -3, -2, -1, 0, -3); this.lineSides.push(3, 1, 2, 3, 0, 1); return this; } /** * Transforms the geometry's vertices to fit snugly within a 100×100×100 box * centered at the origin. * * Calling `myGeometry.normalize()` translates the geometry's vertices so that * they're centered at the origin `(0, 0, 0)`. Then it scales the vertices so * that they fill a 100×100×100 box. As a result, small geometries will grow * and large geometries will shrink. * * Note: `myGeometry.normalize()` only works when called in the * setup() function. * * @chainable * * @example * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a very small torus. * myGeometry = buildGeometry(function() {; * torus(1, 0.25); * }); * * // Normalize the torus so its vertices fill * // the range [-100, 100]. * myGeometry.normalize(); * * describe('A white torus rotates slowly against a dark gray background.'); * } * * function draw() { * background(50); * * // Turn on the lights. * lights(); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Style the torus. * noStroke(); * * // Draw the torus. * model(myGeometry); * } */ normalize() { if (this.vertices.length > 0) { // Find the corners of our bounding box const maxPosition = this.vertices[0].copy(); const minPosition = this.vertices[0].copy(); for (let i = 0; i < this.vertices.length; i++) { maxPosition.x = Math.max(maxPosition.x, this.vertices[i].x); minPosition.x = Math.min(minPosition.x, this.vertices[i].x); maxPosition.y = Math.max(maxPosition.y, this.vertices[i].y); minPosition.y = Math.min(minPosition.y, this.vertices[i].y); maxPosition.z = Math.max(maxPosition.z, this.vertices[i].z); minPosition.z = Math.min(minPosition.z, this.vertices[i].z); } const center = Vector.lerp(maxPosition, minPosition, 0.5); const dist = Vector.sub(maxPosition, minPosition); const longestDist = Math.max(Math.max(dist.x, dist.y), dist.z); const scale = 200 / longestDist; for (let i = 0; i < this.vertices.length; i++) { this.vertices[i].sub(center); this.vertices[i].mult(scale); } } return this; } /** Sets the shader's vertex property or attribute variables. * * A vertex property, or vertex attribute, is a variable belonging to a vertex in a shader. p5.js provides some * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are * set using vertex(), normal() * and fill() respectively. Custom properties can also * be defined within beginShape() and * endShape(). * * The first parameter, `propertyName`, is a string with the property's name. * This is the same variable name which should be declared in the shader, as in * `in vec3 aProperty`, similar to .`setUniform()`. * * The second parameter, `data`, is the value assigned to the shader variable. This value * will be pushed directly onto the Geometry object. There should be the same number * of custom property values as vertices, this method should be invoked once for each * vertex. * * The `data` can be a Number or an array of numbers. Tn the shader program the type * can be declared according to the WebGL specification. Common types include `float`, * `vec2`, `vec3`, `vec4` or matrices. * * See also the global vertexProperty() function. * * @example * let geo; * * function cartesianToSpherical(x, y, z) { * let r = sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); * let theta = acos(z / r); * let phi = atan2(y, x); * return { theta, phi }; * } * * function setup() { * createCanvas(100, 100, WEBGL); * * // Modify the material shader to display roughness. * const myShader = baseMaterialShader().modify({ * vertexDeclarations:`in float aRoughness; * out float vRoughness;`, * fragmentDeclarations: 'in float vRoughness;', * 'void afterVertex': `() { * vRoughness = aRoughness; * }`, * 'vec4 combineColors': `(ColorComponents components) { * vec4 color = vec4(0.); * color.rgb += components.diffuse * components.baseColor * (1.0-vRoughness); * color.rgb += components.ambient * components.ambientColor; * color.rgb += components.specular * components.specularColor * (1.0-vRoughness); * color.a = components.opacity; * return color; * }` * }); * * // Create the Geometry object. * geo = buildGeometry(function() { * fill('hotpink'); * sphere(45, 50, 50); * }); * * // Set the roughness value for every vertex. * for (let v of geo.vertices){ * * // convert coordinates to spherical coordinates * let spherical = cartesianToSpherical(v.x, v.y, v.z); * * // Set the custom roughness vertex property. * let roughness = noise(spherical.theta*5, spherical.phi*5); * geo.vertexProperty('aRoughness', roughness); * } * * // Use the custom shader. * shader(myShader); * * describe('A rough pink sphere rotating on a blue background.'); * } * * function draw() { * // Set some styles and lighting * background('lightblue'); * noStroke(); * * specularMaterial(255,125,100); * shininess(2); * * directionalLight('white', -1, 1, -1); * ambientLight(320); * * rotateY(millis()*0.001); * * // Draw the geometry * model(geo); * } * * @param {String} propertyName the name of the vertex property. * @param {Number|Number[]} data the data tied to the vertex property. * @param {Number} [size] optional size of each unit of data. */ vertexProperty(propertyName, data, size){ let prop; if (!this.userVertexProperties[propertyName]){ prop = this.userVertexProperties[propertyName] = this._userVertexPropertyHelper(propertyName, data, size); } prop = this.userVertexProperties[propertyName]; if (size){ prop.pushDirect(data); } else { prop.setCurrentData(data); prop.pushCurrentData(); } } _userVertexPropertyHelper(propertyName, data, size){ const geometryInstance = this; const prop = this.userVertexProperties[propertyName] = { name: propertyName, dataSize: size ? size : data.length ? data.length : 1, geometry: geometryInstance, // Getters getName(){ return this.name; }, getCurrentData(){ if (this.currentData === undefined) { this.currentData = new Array(this.getDataSize()).fill(0); } return this.currentData; }, getDataSize() { return this.dataSize; }, getSrcName() { const src = this.name.concat('Src'); return src; }, getDstName() { const dst = this.name.concat('Buffer'); return dst; }, getSrcArray() { const srcName = this.getSrcName(); return this.geometry[srcName]; }, //Setters setCurrentData(data) { // if (size != this.getDataSize()){ // p5._friendlyError(`Custom vertex property '${this.name}' has been set with various data sizes. You can change it's name, or if it was an accident, set '${this.name}' to have the same number of inputs each time!`, 'vertexProperty()'); // } this.currentData = data; }, // Utilities pushCurrentData(){ const data = this.getCurrentData(); this.pushDirect(data); }, pushDirect(data) { if (data.length){ this.getSrcArray().push(...data); } else { this.getSrcArray().push(data); } }, resetSrcArray(){ this.geometry[this.getSrcName()] = []; }, delete() { const srcName = this.getSrcName(); delete this.geometry[srcName]; delete this; } }; this[prop.getSrcName()] = []; return this.userVertexProperties[propertyName]; } } /** * Keeps track of how many custom geometry objects have been made so that each * can be assigned a unique ID. */ Geometry.nextId = 0; function geometry(p5, fn){ /** * A class to describe a 3D shape. * * Each `p5.Geometry` object represents a 3D shape as a set of connected * points called *vertices*. All 3D shapes are made by connecting vertices to * form triangles that are stitched together. Each triangular patch on the * geometry's surface is called a *face*. The geometry stores information * about its vertices and faces for use with effects such as lighting and * texture mapping. * * The first parameter, `detailX`, is optional. If a number is passed, as in * `new p5.Geometry(24)`, it sets the number of triangle subdivisions to use * along the geometry's x-axis. By default, `detailX` is 1. * * The second parameter, `detailY`, is also optional. If a number is passed, * as in `new p5.Geometry(24, 16)`, it sets the number of triangle * subdivisions to use along the geometry's y-axis. By default, `detailX` is * 1. * * The third parameter, `callback`, is also optional. If a function is passed, * as in `new p5.Geometry(24, 16, createShape)`, it will be called once to add * vertices to the new 3D shape. * * @class p5.Geometry * @param {Integer} [detailX] number of vertices along the x-axis. * @param {Integer} [detailY] number of vertices along the y-axis. * @param {Function} [callback] function to call once the geometry is created. * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = new p5.Geometry(); * * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(40, 0, 0); * * // Add the vertices to the p5.Geometry object's vertices array. * myGeometry.vertices.push(v0, v1, v2); * * describe('A white triangle drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object using a callback function. * myGeometry = new p5.Geometry(1, 1, createShape); * * describe('A white triangle drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * function createShape() { * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(40, 0, 0); * * // "this" refers to the p5.Geometry object being created. * * // Add the vertices to the p5.Geometry object's vertices array. * this.vertices.push(v0, v1, v2); * * // Add an array to list which vertices belong to the face. * // Vertices are listed in clockwise "winding" order from * // left to top to right. * this.faces.push([0, 1, 2]); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object using a callback function. * myGeometry = new p5.Geometry(1, 1, createShape); * * describe('A white triangle drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * function createShape() { * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(40, 0, 0); * * // "this" refers to the p5.Geometry object being created. * * // Add the vertices to the p5.Geometry object's vertices array. * this.vertices.push(v0, v1, v2); * * // Add an array to list which vertices belong to the face. * // Vertices are listed in clockwise "winding" order from * // left to top to right. * this.faces.push([0, 1, 2]); * * // Compute the surface normals to help with lighting. * this.computeNormals(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * // Adapted from Paul Wheeler's wonderful p5.Geometry tutorial. * // https://www.paulwheeler.us/articles/custom-3d-geometry-in-p5js/ * // CC-BY-SA 4.0 * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the p5.Geometry object. * // Set detailX to 48 and detailY to 2. * // >>> try changing them. * myGeometry = new p5.Geometry(48, 2, createShape); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the p5.Geometry object. * strokeWeight(0.2); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * function createShape() { * // "this" refers to the p5.Geometry object being created. * * // Define the Möbius strip with a few parameters. * let spread = 0.1; * let radius = 30; * let stripWidth = 15; * let xInterval = 4 * PI / this.detailX; * let yOffset = -stripWidth / 2; * let yInterval = stripWidth / this.detailY; * * for (let j = 0; j <= this.detailY; j += 1) { * // Calculate the "vertical" point along the strip. * let v = yOffset + yInterval * j; * * for (let i = 0; i <= this.detailX; i += 1) { * // Calculate the angle of rotation around the strip. * let u = i * xInterval; * * // Calculate the coordinates of the vertex. * let x = (radius + v * cos(u / 2)) * cos(u) - sin(u / 2) * 2 * spread; * let y = (radius + v * cos(u / 2)) * sin(u); * if (u < TWO_PI) { * y += sin(u) * spread; * } else { * y -= sin(u) * spread; * } * let z = v * sin(u / 2) + sin(u / 4) * 4 * spread; * * // Create a p5.Vector object to position the vertex. * let vert = createVector(x, y, z); * * // Add the vertex to the p5.Geometry object's vertices array. * this.vertices.push(vert); * } * } * * // Compute the faces array. * this.computeFaces(); * * // Compute the surface normals to help with lighting. * this.computeNormals(); * } */ p5.Geometry = Geometry; /** * An array with the geometry's vertices. * * The geometry's vertices are stored as * p5.Vector objects in the `myGeometry.vertices` * array. The geometry's first vertex is the * p5.Vector object at `myGeometry.vertices[0]`, * its second vertex is `myGeometry.vertices[1]`, its third vertex is * `myGeometry.vertices[2]`, and so on. * * @property vertices * @for p5.Geometry * @name vertices * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = new p5.Geometry(); * * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(40, 0, 0); * * // Add the vertices to the p5.Geometry object's vertices array. * myGeometry.vertices.push(v0, v1, v2); * * describe('A white triangle drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the p5.Geometry object. * model(myGeometry); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = buildGeometry(function() { * torus(30, 15, 10, 8); * }); * * describe('A white torus rotates slowly against a dark gray background. Red spheres mark its vertices.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateY(frameCount * 0.01); * * // Style the p5.Geometry object. * fill(255); * stroke(0); * * // Display the p5.Geometry object. * model(myGeometry); * * // Style the vertices. * fill(255, 0, 0); * noStroke(); * * // Iterate over the vertices array. * for (let v of myGeometry.vertices) { * // Draw a sphere to mark the vertex. * push(); * translate(v); * sphere(2.5); * pop(); * } * } */ /** * An array with the vectors that are normal to the geometry's vertices. * * A face's orientation is defined by its *normal vector* which points out * of the face and is normal (perpendicular) to the surface. Calling * `myGeometry.computeNormals()` first calculates each face's normal * vector. Then it calculates the normal vector for each vertex by * averaging the normal vectors of the faces surrounding the vertex. The * vertex normals are stored as p5.Vector * objects in the `myGeometry.vertexNormals` array. * * @property vertexNormals * @name vertexNormals * @for p5.Geometry * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = buildGeometry(function() { * torus(30, 15, 10, 8); * }); * * // Compute the vertex normals. * myGeometry.computeNormals(); * * describe( * 'A white torus rotates against a dark gray background. Red lines extend outward from its vertices.' * ); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateY(frameCount * 0.01); * * // Style the p5.Geometry object. * stroke(0); * * // Display the p5.Geometry object. * model(myGeometry); * * // Style the normal vectors. * stroke(255, 0, 0); * * // Iterate over the vertices and vertexNormals arrays. * for (let i = 0; i < myGeometry.vertices.length; i += 1) { * * // Get the vertex p5.Vector object. * let v = myGeometry.vertices[i]; * * // Get the vertex normal p5.Vector object. * let n = myGeometry.vertexNormals[i]; * * // Calculate a point along the vertex normal. * let p = p5.Vector.mult(n, 8); * * // Draw the vertex normal as a red line. * push(); * translate(v); * line(0, 0, 0, p.x, p.y, p.z); * pop(); * } * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = new p5.Geometry(); * * // Create p5.Vector objects to position the vertices. * let v0 = createVector(-40, 0, 0); * let v1 = createVector(0, -40, 0); * let v2 = createVector(0, 40, 0); * let v3 = createVector(40, 0, 0); * * // Add the vertices to the p5.Geometry object's vertices array. * myGeometry.vertices.push(v0, v1, v2, v3); * * // Compute the faces array. * myGeometry.computeFaces(); * * // Compute the surface normals. * myGeometry.computeNormals(); * * describe('A red square drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a white point light. * pointLight(255, 255, 255, 0, 0, 10); * * // Style the p5.Geometry object. * noStroke(); * fill(255, 0, 0); * * // Display the p5.Geometry object. * model(myGeometry); * } */ /** * An array that lists which of the geometry's vertices form each of its * faces. * * All 3D shapes are made by connecting sets of points called *vertices*. A * geometry's surface is formed by connecting vertices to form triangles * that are stitched together. Each triangular patch on the geometry's * surface is called a *face*. * * The geometry's vertices are stored as * p5.Vector objects in the * myGeometry.vertices array. The * geometry's first vertex is the p5.Vector * object at `myGeometry.vertices[0]`, its second vertex is * `myGeometry.vertices[1]`, its third vertex is `myGeometry.vertices[2]`, * and so on. * * For example, a geometry made from a rectangle has two faces because a * rectangle is made by joining two triangles. `myGeometry.faces` for a * rectangle would be the two-dimensional array `[[0, 1, 2], [2, 1, 3]]`. * The first face, `myGeometry.faces[0]`, is the array `[0, 1, 2]` because * it's formed by connecting `myGeometry.vertices[0]`, * `myGeometry.vertices[1]`,and `myGeometry.vertices[2]`. The second face, * `myGeometry.faces[1]`, is the array `[2, 1, 3]` because it's formed by * connecting `myGeometry.vertices[2]`, `myGeometry.vertices[1]`,and * `myGeometry.vertices[3]`. * * @property faces * @name faces * @for p5.Geometry * * @example * // Click and drag the mouse to view the scene from different angles. * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. * myGeometry = buildGeometry(function() { * sphere(); * }); * * describe("A sphere drawn on a gray background. The sphere's surface is a grayscale patchwork of triangles."); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the p5.Geometry object. * noStroke(); * * // Set a random seed. * randomSeed(1234); * * // Iterate over the faces array. * for (let face of myGeometry.faces) { * * // Style the face. * let g = random(0, 255); * fill(g); * * // Draw the face. * beginShape(); * // Iterate over the vertices that form the face. * for (let f of face) { * // Get the vertex's p5.Vector object. * let v = myGeometry.vertices[f]; * vertex(v.x, v.y, v.z); * } * endShape(); * * } * } */ /** * An array that lists the texture coordinates for each of the geometry's * vertices. * * In order for texture() to work, the geometry * needs a way to map the points on its surface to the pixels in a * rectangular image that's used as a texture. The geometry's vertex at * coordinates `(x, y, z)` maps to the texture image's pixel at coordinates * `(u, v)`. * * The `myGeometry.uvs` array stores the `(u, v)` coordinates for each * vertex in the order it was added to the geometry. For example, the * first vertex, `myGeometry.vertices[0]`, has its `(u, v)` coordinates * stored at `myGeometry.uvs[0]` and `myGeometry.uvs[1]`. * * @property uvs * @name uvs * @for p5.Geometry * * @example * let img; * * async function setup() { * img = await loadImage('assets/laDefense.jpg'); * createCanvas(100, 100, WEBGL); * * background(200); * * // Create p5.Geometry objects. * let geom1 = buildGeometry(createShape); * let geom2 = buildGeometry(createShape); * * // Left (original). * push(); * translate(-25, 0, 0); * texture(img); * noStroke(); * model(geom1); * pop(); * * // Set geom2's texture coordinates. * geom2.uvs = [0.25, 0.25, 0.75, 0.25, 0.25, 0.75, 0.75, 0.75]; * * // Right (zoomed in). * push(); * translate(25, 0, 0); * texture(img); * noStroke(); * model(geom2); * pop(); * * describe( * 'Two photos of a ceiling on a gray background. The photo on the right zooms in to the center of the photo.' * ); * } * * function createShape() { * plane(40); * } */ /** * A unique identifier for this geometry. The renderer will use this to cache resources. * * @property {String} gid * @for p5.Geometry */ } if(typeof p5 !== 'undefined'){ geometry(p5); } /** * @private * A class responsible for converting successive WebGL draw calls into a single * `p5.Geometry` that can be reused and drawn with `model()`. */ class GeometryBuilder { constructor(renderer) { this.renderer = renderer; renderer._pInst.push(); this.identityMatrix = new Matrix(4); renderer.states.setValue('uModelMatrix', new Matrix(4)); this.geometry = new Geometry( undefined, undefined, undefined, this.renderer ); this.geometry.gid = `_p5_GeometryBuilder_${GeometryBuilder.nextGeometryId}`; GeometryBuilder.nextGeometryId++; this.hasTransform = false; } /** * @private * Applies the current transformation matrix to each vertex. */ transformVertices(vertices) { if (!this.hasTransform) return vertices; return vertices.map(v => this.renderer.states.uModelMatrix.multiplyPoint(v) ); } /** * @private * Applies the current normal matrix to each normal. */ transformNormals(normals) { if (!this.hasTransform) return normals; return normals.map( v => this.renderer.scratchMat3.multiplyVec(v) // this is a vec3 ); } /** * @private * Adds a p5.Geometry to the builder's combined geometry, flattening * transformations. */ addGeometry(input) { this.hasTransform = !this.renderer.states.uModelMatrix.mat4 .every((v, i) => v === this.identityMatrix.mat4[i]); if (this.hasTransform) { this.renderer.scratchMat3.inverseTranspose4x4( this.renderer.states.uModelMatrix ); } let startIdx = this.geometry.vertices.length; this.geometry.vertices.push(...this.transformVertices(input.vertices)); this.geometry.vertexNormals.push( ...this.transformNormals(input.vertexNormals) ); this.geometry.uvs.push(...input.uvs); const inputUserVertexProps = input.userVertexProperties; const builtUserVertexProps = this.geometry.userVertexProperties; const numPreviousVertices = this.geometry.vertices.length - input.vertices.length; for (const propName in builtUserVertexProps){ if (propName in inputUserVertexProps){ continue; } const prop = builtUserVertexProps[propName]; const size = prop.getDataSize(); const numMissingValues = size * input.vertices.length; const missingValues = Array(numMissingValues).fill(0); prop.pushDirect(missingValues); } for (const propName in inputUserVertexProps){ const prop = inputUserVertexProps[propName]; const data = prop.getSrcArray(); const size = prop.getDataSize(); if (numPreviousVertices > 0 && !(propName in builtUserVertexProps)){ const numMissingValues = size * numPreviousVertices; const missingValues = Array(numMissingValues).fill(0); this.geometry.vertexProperty(propName, missingValues, size); } this.geometry.vertexProperty(propName, data, size); } if (this.renderer.states.fillColor) { this.geometry.faces.push( ...input.faces.map(f => f.map(idx => idx + startIdx)) ); } if (this.renderer.states.strokeColor) { this.geometry.edges.push( ...input.edges.map(edge => edge.map(idx => idx + startIdx)) ); } const vertexColors = [...input.vertexColors]; while (vertexColors.length < input.vertices.length * 4) { vertexColors.push(...this.renderer.states.curFillColor); } this.geometry.vertexColors.push(...vertexColors); } /** * Adds geometry from the renderer's immediate mode into the builder's * combined geometry. */ addImmediate(geometry, shapeMode, { validateFaces = false } = {}) { const faces = []; if (this.renderer.states.fillColor) { if ( shapeMode === TRIANGLE_STRIP || shapeMode === QUAD_STRIP ) { for (let i = 2; i < geometry.vertices.length; i++) { if (i % 2 === 0) { faces.push([i, i - 1, i - 2]); } else { faces.push([i, i - 2, i - 1]); } } } else if (shapeMode === TRIANGLE_FAN) { for (let i = 2; i < geometry.vertices.length; i++) { faces.push([0, i - 1, i]); } } else if (shapeMode === TRIANGLES) { for (let i = 0; i < geometry.vertices.length; i += 3) { if ( !validateFaces || geometry.vertices[i].copy().sub(geometry.vertices[i+1]) .cross(geometry.vertices[i].copy().sub(geometry.vertices[i+2])) .magSq() > 0 ) { faces.push([i, i + 1, i + 2]); } } } } this.addGeometry(Object.assign({}, geometry, { faces })); } /** * Adds geometry from the renderer's retained mode into the builder's * combined geometry. */ addRetained(geometry) { this.addGeometry(geometry); } /** * Cleans up the state of the renderer and returns the combined geometry that * was built. * @returns p5.Geometry The flattened, combined geometry */ finish() { this.renderer._pInst.pop(); return this.geometry; } } /** * Keeps track of how many custom geometry objects have been made so that each * can be assigned a unique ID. */ GeometryBuilder.nextGeometryId = 0; /** * @module Math * @submodule Quaternion */ class Quat { constructor(w, x, y, z) { this.w = w; this.vec = new Vector(x, y, z); } /** * Returns a Quaternion for the * axis angle representation of the rotation * * @method fromAxisAngle * @param {Number} [angle] Angle with which the points needs to be rotated * @param {Number} [x] x component of the axis vector * @param {Number} [y] y component of the axis vector * @param {Number} [z] z component of the axis vector * @chainable */ static fromAxisAngle(angle, x, y, z) { const w = Math.cos(angle/2); const vec = new Vector(x, y, z).normalize().mult(Math.sin(angle/2)); return new Quat(w, vec.x, vec.y, vec.z); } conjugate() { return new Quat(this.w, -this.vec.x, -this.vec.y, -this.vec.z); } /** * Multiplies a quaternion with other quaternion. * @method mult * @param {p5.Quat} [quat] quaternion to multiply with the quaternion calling the method. * @chainable */ multiply(quat) { return new Quat( this.w * quat.w - this.vec.x * quat.vec.x - this.vec.y * quat.vec.y - this.vec.z - quat.vec.z, this.w * quat.vec.x + this.vec.x * quat.w + this.vec.y * quat.vec.z - this.vec.z * quat.vec.y, this.w * quat.vec.y - this.vec.x * quat.vec.z + this.vec.y * quat.w + this.vec.z * quat.vec.x, this.w * quat.vec.z + this.vec.x * quat.vec.y - this.vec.y * quat.vec.x + this.vec.z * quat.w ); } /** * This is similar to quaternion multiplication * but when multipying vector with quaternion * the multiplication can be simplified to the below formula. * This was taken from the below stackexchange link * https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion/50545#50545 * @private * @param {p5.Vector} [p] vector to rotate on the axis quaternion */ rotateVector(p) { return Vector.mult( p, this.w*this.w - this.vec.dot(this.vec) ) .add( Vector.mult( this.vec, 2 * p.dot(this.vec) ) ) .add( Vector.mult( this.vec, 2 * this.w ).cross( p ) ) .clampToZero(); } /** * Rotates the Quaternion by the quaternion passed * which contains the axis of roation and angle of rotation * * @method rotateBy * @param {p5.Quat} [axesQuat] axis quaternion which contains * the axis of rotation and angle of rotation * @chainable */ rotateBy(axesQuat) { return axesQuat.multiply(this).multiply(axesQuat.conjugate()). vec.clampToZero(); } } function quat(p5, fn){ /** * A class to describe a Quaternion * for vector rotations in the p5js webgl renderer. * Please refer the following link for details on the implementation * https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html * @class p5.Quat * @constructor * @param {Number} [w] Scalar part of the quaternion * @param {Number} [x] x component of imaginary part of quaternion * @param {Number} [y] y component of imaginary part of quaternion * @param {Number} [z] z component of imaginary part of quaternion * @private */ p5.Quat = Quat; } if(typeof p5 !== 'undefined'){ quat(p5); } /** * @module 3D * @submodule Camera * @requires core */ class Camera { constructor(renderer) { this._renderer = renderer; this.cameraType = 'default'; this.useLinePerspective = true; this.cameraMatrix = new Matrix(4); this.projMatrix = new Matrix(4); this.yScale = 1; } //////////////////////////////////////////////////////////////////////////////// // Camera Projection Methods //////////////////////////////////////////////////////////////////////////////// /** * Sets a perspective projection for the camera. * * In a perspective projection, shapes that are further from the camera appear * smaller than shapes that are near the camera. This technique, called * foreshortening, creates realistic 3D scenes. It’s applied by default in new * `p5.Camera` objects. * * `myCamera.perspective()` changes the camera’s perspective by changing its * viewing frustum. The frustum is the volume of space that’s visible to the * camera. The frustum’s shape is a pyramid with its top cut off. The camera * is placed where the top of the pyramid should be and points towards the * base of the pyramid. It views everything within the frustum. * * The first parameter, `fovy`, is the camera’s vertical field of view. It’s * an angle that describes how tall or narrow a view the camera has. For * example, calling `myCamera.perspective(0.5)` sets the camera’s vertical * field of view to 0.5 radians. By default, `fovy` is calculated based on the * sketch’s height and the camera’s default z-coordinate, which is 800. The * formula for the default `fovy` is `2 * atan(height / 2 / 800)`. * * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number * that describes the ratio of the top plane’s width to its height. For * example, calling `myCamera.perspective(0.5, 1.5)` sets the camera’s field * of view to 0.5 radians and aspect ratio to 1.5, which would make shapes * appear thinner on a square canvas. By default, `aspect` is set to * `width / height`. * * The third parameter, `near`, is the distance from the camera to the near * plane. For example, calling `myCamera.perspective(0.5, 1.5, 100)` sets the * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, and places * the near plane 100 pixels from the camera. Any shapes drawn less than 100 * pixels from the camera won’t be visible. By default, `near` is set to * `0.1 * 800`, which is 1/10th the default distance between the camera and * the origin. * * The fourth parameter, `far`, is the distance from the camera to the far * plane. For example, calling `myCamera.perspective(0.5, 1.5, 100, 10000)` * sets the camera’s field of view to 0.5 radians, its aspect ratio to 1.5, * places the near plane 100 pixels from the camera, and places the far plane * 10,000 pixels from the camera. Any shapes drawn more than 10,000 pixels * from the camera won’t be visible. By default, `far` is set to `10 * 800`, * which is 10 times the default distance between the camera and the origin. * * @for p5.Camera * @param {Number} [fovy] camera frustum vertical field of view. Defaults to * `2 * atan(height / 2 / 800)`. * @param {Number} [aspect] camera frustum aspect ratio. Defaults to * `width / height`. * @param {Number} [near] distance from the camera to the near clipping plane. * Defaults to `0.1 * 800`. * @param {Number} [far] distance from the camera to the far clipping plane. * Defaults to `10 * 800`. * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Place it at the top-right. * cam2.camera(400, -400, 800); * * // Set its fovy to 0.2. * // Set its aspect to 1.5. * // Set its near to 600. * // Set its far to 1200. * cam2.perspective(0.2, 1.5, 600, 1200); * * // Set the current camera to cam1. * setCamera(cam1); * * describe('A white cube on a gray background. The camera toggles between a frontal view and a skewed aerial view when the user double-clicks.'); * } * * function draw() { * background(200); * * // Draw the box. * box(); * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Place it at the top-right. * cam2.camera(400, -400, 800); * * // Set its fovy to 0.2. * // Set its aspect to 1.5. * // Set its near to 600. * // Set its far to 1200. * cam2.perspective(0.2, 1.5, 600, 1200); * * // Set the current camera to cam1. * setCamera(cam1); * * describe('A white cube moves left and right on a gray background. The camera toggles between a frontal and a skewed aerial view when the user double-clicks.'); * } * * function draw() { * background(200); * * // Translate the origin left and right. * let x = 100 * sin(frameCount * 0.01); * translate(x, 0, 0); * * // Draw the box. * box(); * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } */ perspective(fovy, aspect, near, far) { const range = this._renderer.zClipRange(); this.cameraType = arguments.length > 0 ? 'custom' : 'default'; if (typeof fovy === 'undefined') { fovy = this.defaultCameraFOV; // this avoids issue where setting angleMode(DEGREES) before calling // perspective leads to a smaller than expected FOV (because // _computeCameraDefaultSettings computes in radians) this.cameraFOV = fovy; } else { this.cameraFOV = this._renderer._pInst._toRadians(fovy); } if (typeof aspect === 'undefined') { aspect = this.defaultAspectRatio; } if (typeof near === 'undefined') { near = this.defaultCameraNear; } if (typeof far === 'undefined') { far = this.defaultCameraFar; } if (near <= 0.0001) { near = 0.01; console.log( 'Avoid perspective near plane values close to or below 0. ' + 'Setting value to 0.01.' ); } if (far < near) { console.log( 'Perspective far plane value is less than near plane value. ' + 'Nothing will be shown.' ); } this.aspectRatio = aspect; this.cameraNear = near; this.cameraFar = far; this.projMatrix = new Matrix(4); const f = 1.0 / Math.tan(this.cameraFOV / 2); const nf = 1.0 / (this.cameraNear - this.cameraFar); let A, B; if (range[0] === 0) { // WebGPU clip space, z in [0, 1] A = far / (near - far); B = (far * near) / (near - far); } else { // WebGL clip space, z in [-1, 1] A = (far + near) * nf; B = (2 * far * near) * nf; } this.projMatrix.set(f / aspect, 0, 0, 0, 0, -f * this.yScale, 0, 0, 0, 0, A, -1, 0, 0, B, 0); if (this._isActive()) { this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone()); this._renderer.states.uPMatrix.set(this.projMatrix); } } /** * Sets an orthographic projection for the camera. * * In an orthographic projection, shapes with the same size always appear the * same size, regardless of whether they are near or far from the camera. * * `myCamera.ortho()` changes the camera’s perspective by changing its viewing * frustum from a truncated pyramid to a rectangular prism. The frustum is the * volume of space that’s visible to the camera. The camera is placed in front * of the frustum and views everything within the frustum. `myCamera.ortho()` * has six optional parameters to define the viewing frustum. * * The first four parameters, `left`, `right`, `bottom`, and `top`, set the * coordinates of the frustum’s sides, bottom, and top. For example, calling * `myCamera.ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels * wide and 400 pixels tall. By default, these dimensions are set based on * the sketch’s width and height, as in * `myCamera.ortho(-width / 2, width / 2, -height / 2, height / 2)`. * * The last two parameters, `near` and `far`, set the distance of the * frustum’s near and far plane from the camera. For example, calling * `myCamera.ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s * 200 pixels wide, 400 pixels tall, starts 50 pixels from the camera, and * ends 1,000 pixels from the camera. By default, `near` and `far` are set to * 0 and `max(width, height) + 800`, respectively. * * @for p5.Camera * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 2`. * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 2`. * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`. * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 2`. * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to 0. * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`. * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Apply an orthographic projection. * cam2.ortho(); * * // Set the current camera to cam1. * setCamera(cam1); * * describe('A row of white cubes against a gray background. The camera toggles between a perspective and an orthographic projection when the user double-clicks.'); * } * * function draw() { * background(200); * * // Translate the origin toward the camera. * translate(-10, 10, 500); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Apply an orthographic projection. * cam2.ortho(); * * // Set the current camera to cam1. * setCamera(cam1); * * describe('A row of white cubes slither like a snake against a gray background. The camera toggles between a perspective and an orthographic projection when the user double-clicks.'); * } * * function draw() { * background(200); * * // Translate the origin toward the camera. * translate(-10, 10, 500); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * push(); * // Calculate the box's coordinates. * let x = 10 * sin(frameCount * 0.02 + i * 0.6); * let z = -40 * i; * // Translate the origin. * translate(x, 0, z); * // Draw the box. * box(10); * pop(); * } * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } */ ortho(left, right, bottom, top, near, far) { const source = this.fbo || this._renderer; if (left === undefined) left = -source.width / 2; if (right === undefined) right = +source.width / 2; if (bottom === undefined) bottom = -source.height / 2; if (top === undefined) top = +source.height / 2; if (near === undefined) near = 0; if (far === undefined) far = Math.max(source.width, source.height) + 800; this.cameraNear = near; this.cameraFar = far; const w = right - left; const h = top - bottom; const d = far - near; const x = 2 / w; const y = 2 / h * this.yScale; const z = -2 / d; const tx = -(right + left) / w; const ty = -(top + bottom) / h; const tz = -(far + near) / d; this.projMatrix = new Matrix(4); this.projMatrix.set(x, 0, 0, 0, 0, -y, 0, 0, 0, 0, z, 0, tx, ty, tz, 1); if (this._isActive()) { this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone()); this._renderer.states.uPMatrix.set(this.projMatrix); } this.cameraType = 'custom'; } /** * Sets the camera's frustum. * * In a frustum projection, shapes that are further from the camera appear * smaller than shapes that are near the camera. This technique, called * foreshortening, creates realistic 3D scenes. * * `myCamera.frustum()` changes the camera’s perspective by changing its * viewing frustum. The frustum is the volume of space that’s visible to the * camera. The frustum’s shape is a pyramid with its top cut off. The camera * is placed where the top of the pyramid should be and points towards the * base of the pyramid. It views everything within the frustum. * * The first four parameters, `left`, `right`, `bottom`, and `top`, set the * coordinates of the frustum’s sides, bottom, and top. For example, calling * `myCamera.frustum(-100, 100, 200, -200)` creates a frustum that’s 200 * pixels wide and 400 pixels tall. By default, these coordinates are set * based on the sketch’s width and height, as in * `myCamera.frustum(-width / 20, width / 20, height / 20, -height / 20)`. * * The last two parameters, `near` and `far`, set the distance of the * frustum’s near and far plane from the camera. For example, calling * `myCamera.frustum(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s * 200 pixels wide, 400 pixels tall, starts 50 pixels from the camera, and ends * 1,000 pixels from the camera. By default, near is set to `0.1 * 800`, which * is 1/10th the default distance between the camera and the origin. `far` is * set to `10 * 800`, which is 10 times the default distance between the * camera and the origin. * * @for p5.Camera * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 20`. * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 20`. * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`. * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 20`. * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`. * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `10 * 800`. * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Adjust the frustum. * // Center it. * // Set its width and height to 20 pixels. * // Place its near plane 300 pixels from the camera. * // Place its far plane 350 pixels from the camera. * cam2.frustum(-10, 10, -10, 10, 300, 350); * * // Set the current camera to cam1. * setCamera(cam1); * * describe( * 'A row of white cubes against a gray background. The camera zooms in on one cube when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Translate the origin toward the camera. * translate(-10, 10, 600); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } */ frustum(left, right, bottom, top, near, far) { if (left === undefined) left = -this._renderer.width * 0.05; if (right === undefined) right = +this._renderer.width * 0.05; if (bottom === undefined) bottom = +this._renderer.height * 0.05; if (top === undefined) top = -this._renderer.height * 0.05; if (near === undefined) near = this.defaultCameraNear; if (far === undefined) far = this.defaultCameraFar; this.cameraNear = near; this.cameraFar = far; const w = right - left; const h = top - bottom; const d = far - near; const x = +(2.0 * near) / w; const y = +(2.0 * near) / h * this.yScale; const z = -(2.0 * far * near) / d; const tx = (right + left) / w; const ty = (top + bottom) / h; const tz = -(far + near) / d; this.projMatrix = new Matrix(4); this.projMatrix.set(x, 0, 0, 0, 0, -y, 0, 0, tx, ty, tz, -1, 0, 0, z, 0); if (this._isActive()) { this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone()); this._renderer.states.uPMatrix.set(this.projMatrix); } this.cameraType = 'custom'; } //////////////////////////////////////////////////////////////////////////////// // Camera Orientation Methods //////////////////////////////////////////////////////////////////////////////// /** * Rotate camera view about arbitrary axis defined by x,y,z * based on http://learnwebgl.brown37.net/07_cameras/camera_rotating_motion.html * @private */ _rotateView(a, x, y, z) { let centerX = this.centerX; let centerY = this.centerY; let centerZ = this.centerZ; // move center by eye position such that rotation happens around eye position centerX -= this.eyeX; centerY -= this.eyeY; centerZ -= this.eyeZ; const rotation = new Matrix(4); // TODO Maybe pass p5 rotation.rotate4x4(this._renderer._pInst._toRadians(a), x, y, z); const rotatedCenter = [ centerX * rotation.mat4[0] + centerY * rotation.mat4[4] + centerZ * rotation.mat4[8], centerX * rotation.mat4[1] + centerY * rotation.mat4[5] + centerZ * rotation.mat4[9], centerX * rotation.mat4[2] + centerY * rotation.mat4[6] + centerZ * rotation.mat4[10] ]; // add eye position back into center rotatedCenter[0] += this.eyeX; rotatedCenter[1] += this.eyeY; rotatedCenter[2] += this.eyeZ; this.camera( this.eyeX, this.eyeY, this.eyeZ, rotatedCenter[0], rotatedCenter[1], rotatedCenter[2], this.upX, this.upY, this.upZ ); } /** * Rotates the camera in a clockwise/counter-clockwise direction. * * Rolling rotates the camera without changing its orientation. The rotation * happens in the camera’s "local" space. * * The parameter, `angle`, is the angle the camera should rotate. Passing a * positive angle, as in `myCamera.roll(0.001)`, rotates the camera in counter-clockwise direction. * Passing a negative angle, as in `myCamera.roll(-0.001)`, rotates the * camera in clockwise direction. * * Note: Angles are interpreted based on the current * angleMode(). * * @method roll * @param {Number} angle amount to rotate camera in current * angleMode units. * @example * let cam; * let delta = 0.01; * * function setup() { * createCanvas(100, 100, WEBGL); * normalMaterial(); * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * } * * function draw() { * background(200); * * // Roll camera according to angle 'delta' * cam.roll(delta); * * translate(0, 0, 0); * box(20); * translate(0, 25, 0); * box(20); * translate(0, 26, 0); * box(20); * translate(0, 27, 0); * box(20); * translate(0, 28, 0); * box(20); * translate(0,29, 0); * box(20); * translate(0, 30, 0); * box(20); * } * * @alt * camera view rotates in counter clockwise direction with vertically stacked boxes in front of it. */ roll(amount) { const local = this._getLocalAxes(); const axisQuaternion = Quat.fromAxisAngle( this._renderer._pInst._toRadians(amount), local.z[0], local.z[1], local.z[2]); // const upQuat = new p5.Quat(0, this.upX, this.upY, this.upZ); const newUpVector = axisQuaternion.rotateVector( new Vector(this.upX, this.upY, this.upZ)); this.camera( this.eyeX, this.eyeY, this.eyeZ, this.centerX, this.centerY, this.centerZ, newUpVector.x, newUpVector.y, newUpVector.z ); } /** * Rotates the camera left and right. * * Panning rotates the camera without changing its position. The rotation * happens in the camera’s "local" space. * * The parameter, `angle`, is the angle the camera should rotate. Passing a * positive angle, as in `myCamera.pan(0.001)`, rotates the camera to the * right. Passing a negative angle, as in `myCamera.pan(-0.001)`, rotates the * camera to the left. * * Note: Angles are interpreted based on the current * angleMode(). * * @param {Number} angle amount to rotate in the current * angleMode(). * * @example * let cam; * let delta = 0.001; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.' * ); * } * * function draw() { * background(200); * * // Pan with the camera. * cam.pan(delta); * * // Switch directions every 120 frames. * if (frameCount % 120 === 0) { * delta *= -1; * } * * // Draw the box. * box(); * } */ pan(amount) { const local = this._getLocalAxes(); this._rotateView(amount, local.y[0], local.y[1], local.y[2]); } /** * Rotates the camera up and down. * * Tilting rotates the camera without changing its position. The rotation * happens in the camera’s "local" space. * * The parameter, `angle`, is the angle the camera should rotate. Passing a * positive angle, as in `myCamera.tilt(0.001)`, rotates the camera down. * Passing a negative angle, as in `myCamera.tilt(-0.001)`, rotates the camera * up. * * Note: Angles are interpreted based on the current * angleMode(). * * @param {Number} angle amount to rotate in the current * angleMode(). * * @example * let cam; * let delta = 0.001; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The cube goes in and out of view as the camera tilts up and down.' * ); * } * * function draw() { * background(200); * * // Pan with the camera. * cam.tilt(delta); * * // Switch directions every 120 frames. * if (frameCount % 120 === 0) { * delta *= -1; * } * * // Draw the box. * box(); * } */ tilt(amount) { const local = this._getLocalAxes(); this._rotateView(amount, local.x[0], local.x[1], local.x[2]); } /** * Points the camera at a location. * * `myCamera.lookAt()` changes the camera’s orientation without changing its * position. * * The parameters, `x`, `y`, and `z`, are the coordinates in "world" space * where the camera should point. For example, calling * `myCamera.lookAt(10, 20, 30)` points the camera at the coordinates * `(10, 20, 30)`. * * @for p5.Camera * @param {Number} x x-coordinate of the position where the camera should look in "world" space. * @param {Number} y y-coordinate of the position where the camera should look in "world" space. * @param {Number} z z-coordinate of the position where the camera should look in "world" space. * * @example * // Double-click to look at a different cube. * * let cam; * let isLookingLeft = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(-30, 0, 0); * * describe( * 'A red cube and a blue cube on a gray background. The camera switches focus between the cubes when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Draw the box on the left. * push(); * // Translate the origin to the left. * translate(-30, 0, 0); * // Style the box. * fill(255, 0, 0); * // Draw the box. * box(20); * pop(); * * // Draw the box on the right. * push(); * // Translate the origin to the right. * translate(30, 0, 0); * // Style the box. * fill(0, 0, 255); * // Draw the box. * box(20); * pop(); * } * * // Change the camera's focus when the user double-clicks. * function doubleClicked() { * if (isLookingLeft === true) { * cam.lookAt(30, 0, 0); * isLookingLeft = false; * } else { * cam.lookAt(-30, 0, 0); * isLookingLeft = true; * } * } */ lookAt(x, y, z) { this.camera( this.eyeX, this.eyeY, this.eyeZ, x, y, z, this.upX, this.upY, this.upZ ); } //////////////////////////////////////////////////////////////////////////////// // Camera Position Methods //////////////////////////////////////////////////////////////////////////////// /** * Sets the position and orientation of the camera. * * `myCamera.camera()` allows objects to be viewed from different angles. It * has nine parameters that are all optional. * * The first three parameters, `x`, `y`, and `z`, are the coordinates of the * camera’s position in "world" space. For example, calling * `myCamera.camera(0, 0, 0)` places the camera at the origin `(0, 0, 0)`. By * default, the camera is placed at `(0, 0, 800)`. * * The next three parameters, `centerX`, `centerY`, and `centerZ` are the * coordinates of the point where the camera faces in "world" space. For * example, calling `myCamera.camera(0, 0, 0, 10, 20, 30)` places the camera * at the origin `(0, 0, 0)` and points it at `(10, 20, 30)`. By default, the * camera points at the origin `(0, 0, 0)`. * * The last three parameters, `upX`, `upY`, and `upZ` are the components of * the "up" vector in "local" space. The "up" vector orients the camera’s * y-axis. For example, calling * `myCamera.camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up" * vector is `(0, 1, 0)`. * * @for p5.Camera * @param {Number} [x] x-coordinate of the camera. Defaults to 0. * @param {Number} [y] y-coordinate of the camera. Defaults to 0. * @param {Number} [z] z-coordinate of the camera. Defaults to 800. * @param {Number} [centerX] x-coordinate of the point the camera faces. Defaults to 0. * @param {Number} [centerY] y-coordinate of the point the camera faces. Defaults to 0. * @param {Number} [centerZ] z-coordinate of the point the camera faces. Defaults to 0. * @param {Number} [upX] x-component of the camera’s "up" vector. Defaults to 0. * @param {Number} [upY] x-component of the camera’s "up" vector. Defaults to 1. * @param {Number} [upZ] z-component of the camera’s "up" vector. Defaults to 0. * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Place it at the top-right: (1200, -600, 100) * // Point it at the row of boxes: (-10, -10, 400) * // Set its "up" vector to the default: (0, 1, 0) * cam2.camera(1200, -600, 100, -10, -10, 400, 0, 1, 0); * * // Set the current camera to cam1. * setCamera(cam1); * * describe( * 'A row of white cubes against a gray background. The camera toggles between a frontal and an aerial view when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Translate the origin toward the camera. * translate(-10, 10, 500); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -30); * box(10); * } * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Place it at the right: (1200, 0, 100) * // Point it at the row of boxes: (-10, -10, 400) * // Set its "up" vector to the default: (0, 1, 0) * cam2.camera(1200, 0, 100, -10, -10, 400, 0, 1, 0); * * // Set the current camera to cam1. * setCamera(cam1); * * describe( * 'A row of white cubes against a gray background. The camera toggles between a static frontal view and an orbiting view when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Update cam2's position. * let x = 1200 * cos(frameCount * 0.01); * let y = -600 * sin(frameCount * 0.01); * cam2.camera(x, y, 100, -10, -10, 400, 0, 1, 0); * * // Translate the origin toward the camera. * translate(-10, 10, 500); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -30); * box(10); * } * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } */ camera( eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ ) { if (typeof eyeX === 'undefined') { eyeX = this.defaultEyeX; eyeY = this.defaultEyeY; eyeZ = this.defaultEyeZ; centerX = eyeX; centerY = eyeY; centerZ = 0; upX = 0; upY = 1; upZ = 0; } this.eyeX = eyeX; this.eyeY = eyeY; this.eyeZ = eyeZ; if (typeof centerX !== 'undefined') { this.centerX = centerX; this.centerY = centerY; this.centerZ = centerZ; } if (typeof upX !== 'undefined') { this.upX = upX; this.upY = upY; this.upZ = upZ; } const local = this._getLocalAxes(); // the camera affects the model view matrix, insofar as it // inverse translates the world to the eye position of the camera // and rotates it. this.cameraMatrix.set(local.x[0], local.y[0], local.z[0], 0, local.x[1], local.y[1], local.z[1], 0, local.x[2], local.y[2], local.z[2], 0, 0, 0, 0, 1); const tx = -eyeX; const ty = -eyeY; const tz = -eyeZ; this.cameraMatrix.translate([tx, ty, tz]); if (this._isActive()) { this._renderer.states.setValue('uViewMatrix', this._renderer.states.uViewMatrix.clone()); this._renderer.states.uViewMatrix.set(this.cameraMatrix); } return this; } /** * Moves the camera along its "local" axes without changing its orientation. * * The parameters, `x`, `y`, and `z`, are the distances the camera should * move. For example, calling `myCamera.move(10, 20, 30)` moves the camera 10 * pixels to the right, 20 pixels down, and 30 pixels backward in its "local" * space. * * @param {Number} x distance to move along the camera’s "local" x-axis. * @param {Number} y distance to move along the camera’s "local" y-axis. * @param {Number} z distance to move along the camera’s "local" z-axis. * @example * // Click the canvas to begin detecting key presses. * * let cam; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam = createCamera(); * * // Place the camera at the top-right. * cam.setPosition(400, -400, 800); * * // Point it at the origin. * cam.lookAt(0, 0, 0); * * // Set the camera. * setCamera(cam); * * describe( * 'A white cube drawn against a gray background. The cube appears to move when the user presses certain keys.' * ); * } * * function draw() { * background(200); * * // Move the camera along its "local" axes * // when the user presses certain keys. * * // Move horizontally. * if (keyIsDown(LEFT_ARROW)) { * cam.move(-1, 0, 0); * } * if (keyIsDown(RIGHT_ARROW)) { * cam.move(1, 0, 0); * } * * // Move vertically. * if (keyIsDown(UP_ARROW)) { * cam.move(0, -1, 0); * } * if (keyIsDown(DOWN_ARROW)) { * cam.move(0, 1, 0); * } * * // Move in/out of the screen. * if (keyIsDown('i')) { * cam.move(0, 0, -1); * } * if (keyIsDown('o')) { * cam.move(0, 0, 1); * } * * // Draw the box. * box(); * } */ move(x, y, z) { const local = this._getLocalAxes(); // scale local axes by movement amounts // based on http://learnwebgl.brown37.net/07_cameras/camera_linear_motion.html const dx = [local.x[0] * x, local.x[1] * x, local.x[2] * x]; const dy = [local.y[0] * y, local.y[1] * y, local.y[2] * y]; const dz = [local.z[0] * z, local.z[1] * z, local.z[2] * z]; this.camera( this.eyeX + dx[0] + dy[0] + dz[0], this.eyeY + dx[1] + dy[1] + dz[1], this.eyeZ + dx[2] + dy[2] + dz[2], this.centerX + dx[0] + dy[0] + dz[0], this.centerY + dx[1] + dy[1] + dz[1], this.centerZ + dx[2] + dy[2] + dz[2], this.upX, this.upY, this.upZ ); } /** * Sets the camera’s position in "world" space without changing its * orientation. * * The parameters, `x`, `y`, and `z`, are the coordinates where the camera * should be placed. For example, calling `myCamera.setPosition(10, 20, 30)` * places the camera at coordinates `(10, 20, 30)` in "world" space. * * @param {Number} x x-coordinate in "world" space. * @param {Number} y y-coordinate in "world" space. * @param {Number} z z-coordinate in "world" space. * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Place it closer to the origin. * cam2.setPosition(0, 0, 600); * * // Set the current camera to cam1. * setCamera(cam1); * * describe( * 'A row of white cubes against a gray background. The camera toggles the amount of zoom when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Translate the origin toward the camera. * translate(-10, 10, 500); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -30); * box(10); * } * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * cam2 = createCamera(); * * // Place it closer to the origin. * cam2.setPosition(0, 0, 600); * * // Set the current camera to cam1. * setCamera(cam1); * * describe( * 'A row of white cubes against a gray background. The camera toggles between a static view and a view that zooms in and out when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Update cam2's z-coordinate. * let z = 100 * sin(frameCount * 0.01) + 700; * cam2.setPosition(0, 0, z); * * // Translate the origin toward the camera. * translate(-10, 10, 500); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -30); * box(10); * } * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } */ setPosition(x, y, z) { const diffX = x - this.eyeX; const diffY = y - this.eyeY; const diffZ = z - this.eyeZ; this.camera( x, y, z, this.centerX + diffX, this.centerY + diffY, this.centerZ + diffZ, this.upX, this.upY, this.upZ ); } /** * Sets the camera’s position, orientation, and projection by copying another * camera. * * The parameter, `cam`, is the `p5.Camera` object to copy. For example, calling * `cam2.set(cam1)` will set `cam2` using `cam1`’s configuration. * * @param {p5.Camera} cam camera to copy. * * @example * // Double-click to "reset" the camera zoom. * * let cam1; * let cam2; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * cam1 = createCamera(); * * // Place the camera at the top-right. * cam1.setPosition(400, -400, 800); * * // Point it at the origin. * cam1.lookAt(0, 0, 0); * * // Create the second camera. * cam2 = createCamera(); * * // Copy cam1's configuration. * cam2.set(cam1); * * // Set the camera. * setCamera(cam2); * * describe( * 'A white cube drawn against a gray background. The camera slowly moves forward. The camera resets when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Update cam2's position. * cam2.move(0, 0, -1); * * // Draw the box. * box(); * } * * // "Reset" the camera when the user double-clicks. * function doubleClicked() { * cam2.set(cam1); * } */ set(cam) { const keyNamesOfThePropToCopy = [ 'eyeX', 'eyeY', 'eyeZ', 'centerX', 'centerY', 'centerZ', 'upX', 'upY', 'upZ', 'cameraFOV', 'aspectRatio', 'cameraNear', 'cameraFar', 'cameraType', 'yScale', 'useLinePerspective' ]; for (const keyName of keyNamesOfThePropToCopy) { this[keyName] = cam[keyName]; } this.cameraMatrix = cam.cameraMatrix.copy(); this.projMatrix = cam.projMatrix.copy(); if (this._isActive()) { this._renderer.states.setValue('uModelMatrix', this._renderer.states.uModelMatrix.clone()); this._renderer.states.setValue('uViewMatrix', this._renderer.states.uViewMatrix.clone()); this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone()); this._renderer.states.uModelMatrix.reset(); this._renderer.states.uViewMatrix.set(this.cameraMatrix); this._renderer.states.uPMatrix.set(this.projMatrix); } } /** * Sets the camera’s position and orientation to values that are in-between * those of two other cameras. * * `myCamera.slerp()` uses spherical linear interpolation to calculate a * position and orientation that’s in-between two other cameras. Doing so is * helpful for transitioning smoothly between two perspectives. * * The first two parameters, `cam0` and `cam1`, are the `p5.Camera` objects * that should be used to set the current camera. * * The third parameter, `amt`, is the amount to interpolate between `cam0` and * `cam1`. 0.0 keeps the camera’s position and orientation equal to `cam0`’s, * 0.5 sets them halfway between `cam0`’s and `cam1`’s , and 1.0 sets the * position and orientation equal to `cam1`’s. * * For example, calling `myCamera.slerp(cam0, cam1, 0.1)` sets cam’s position * and orientation very close to `cam0`’s. Calling * `myCamera.slerp(cam0, cam1, 0.9)` sets cam’s position and orientation very * close to `cam1`’s. * * Note: All of the cameras must use the same projection. * * @param {p5.Camera} cam0 first camera. * @param {p5.Camera} cam1 second camera. * @param {Number} amt amount of interpolation between 0.0 (`cam0`) and 1.0 (`cam1`). * * @example * let cam; * let cam0; * let cam1; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the main camera. * // Keep its default settings. * cam = createCamera(); * * // Create the first camera. * // Keep its default settings. * cam0 = createCamera(); * * // Create the second camera. * cam1 = createCamera(); * * // Place it at the top-right. * cam1.setPosition(400, -400, 800); * * // Point it at the origin. * cam1.lookAt(0, 0, 0); * * // Set the current camera to cam. * setCamera(cam); * * describe('A white cube drawn against a gray background. The camera slowly oscillates between a frontal view and an aerial view.'); * } * * function draw() { * background(200); * * // Calculate the amount to interpolate between cam0 and cam1. * let amt = 0.5 * sin(frameCount * 0.01) + 0.5; * * // Update the main camera's position and orientation. * cam.slerp(cam0, cam1, amt); * * box(); * } */ slerp(cam0, cam1, amt) { // If t is 0 or 1, do not interpolate and set the argument camera. if (amt === 0) { this.set(cam0); return; } else if (amt === 1) { this.set(cam1); return; } // For this cameras is ortho, assume that cam0 and cam1 are also ortho // and interpolate the elements of the projection matrix. // Use logarithmic interpolation for interpolation. if (this.projMatrix.mat4[15] !== 0) { this.projMatrix.setElement( 0, cam0.projMatrix.mat4[0] * Math.pow(cam1.projMatrix.mat4[0] / cam0.projMatrix.mat4[0], amt) ); this.projMatrix.setElement( 5, cam0.projMatrix.mat4[5] * Math.pow(cam1.projMatrix.mat4[5] / cam0.projMatrix.mat4[5], amt) ); // If the camera is active, make uPMatrix reflect changes in projMatrix. if (this._isActive()) { this._renderer.states.setValue('uPMatrix', this.projMatrix.clone()); } } // prepare eye vector and center vector of argument cameras. const eye0 = new Vector(cam0.eyeX, cam0.eyeY, cam0.eyeZ); const eye1 = new Vector(cam1.eyeX, cam1.eyeY, cam1.eyeZ); const center0 = new Vector(cam0.centerX, cam0.centerY, cam0.centerZ); const center1 = new Vector(cam1.centerX, cam1.centerY, cam1.centerZ); // Calculate the distance between eye and center for each camera. // Logarithmically interpolate these with amt. const dist0 = Vector.dist(eye0, center0); const dist1 = Vector.dist(eye1, center1); const lerpedDist = dist0 * Math.pow(dist1 / dist0, amt); // Next, calculate the ratio to interpolate the eye and center by a constant // ratio for each camera. This ratio is the same for both. Also, with this ratio // of points, the distance is the minimum distance of the two points of // the same ratio. // With this method, if the viewpoint is fixed, linear interpolation is performed // at the viewpoint, and if the center is fixed, linear interpolation is performed // at the center, resulting in reasonable interpolation. If both move, the point // halfway between them is taken. const eyeDiff = Vector.sub(eye0, eye1); const diffDiff = eye0.copy().sub(eye1).sub(center0).add(center1); // Suppose there are two line segments. Consider the distance between the points // above them as if they were taken in the same ratio. This calculation figures out // a ratio that minimizes this. // Each line segment is, a line segment connecting the viewpoint and the center // for each camera. const divider = diffDiff.magSq(); let ratio = 1; // default. if (divider > 0.000001) { ratio = Vector.dot(eyeDiff, diffDiff) / divider; ratio = Math.max(0, Math.min(ratio, 1)); } // Take the appropriate proportions and work out the points // that are between the new viewpoint and the new center position. const lerpedMedium = Vector.lerp( Vector.lerp(eye0, center0, ratio), Vector.lerp(eye1, center1, ratio), amt ); // Prepare each of rotation matrix from their camera matrix const rotMat0 = cam0.cameraMatrix.createSubMatrix3x3(); const rotMat1 = cam1.cameraMatrix.createSubMatrix3x3(); // get front and up vector from local-coordinate-system. const front0 = rotMat0.row(2); const front1 = rotMat1.row(2); const up0 = rotMat0.row(1); const up1 = rotMat1.row(1); // prepare new vectors. const newFront = new Vector(); const newUp = new Vector(); const newEye = new Vector(); const newCenter = new Vector(); // Create the inverse matrix of mat0 by transposing mat0, // and multiply it to mat1 from the right. // This matrix represents the difference between the two. // 'deltaRot' means 'difference of rotation matrices'. const deltaRot = rotMat1.mult(rotMat0.copy().transpose()); // mat1 is 3x3 // Calculate the trace and from it the cos value of the angle. // An orthogonal matrix is just an orthonormal basis. If this is not the identity // matrix, it is a centered orthonormal basis plus some angle of rotation about // some axis. That's the angle. Letting this be theta, trace becomes 1+2cos(theta). // reference: https://en.wikipedia.org/wiki/Rotation_matrix#Determining_the_angle const diag = deltaRot.diagonal(); let cosTheta = 0.5 * (diag[0] + diag[1] + diag[2] - 1); // If the angle is close to 0, the two matrices are very close, // so in that case we execute linearly interpolate. if (1 - cosTheta < 0.0000001) { // Obtain the front vector and up vector by linear interpolation // and normalize them. // calculate newEye, newCenter with newFront vector. newFront.set(Vector.lerp(front0, front1, amt)).normalize(); newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium); newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium); newUp.set(Vector.lerp(up0, up1, amt)).normalize(); // set the camera this.camera( newEye.x, newEye.y, newEye.z, newCenter.x, newCenter.y, newCenter.z, newUp.x, newUp.y, newUp.z ); return; } // Calculates the axis vector and the angle of the difference orthogonal matrix. // The axis vector is what I explained earlier in the comments. // similar calculation is here: // https://github.com/mrdoob/three.js/blob/883249620049d1632e8791732808fefd1a98c871/src/math/Quaternion.js#L294 let a, b, c, sinTheta; let invOneMinusCosTheta = 1 / (1 - cosTheta); const maxDiag = Math.max(diag[0], diag[1], diag[2]); const offDiagSum13 = deltaRot.mat3[1] + deltaRot.mat3[3]; const offDiagSum26 = deltaRot.mat3[2] + deltaRot.mat3[6]; const offDiagSum57 = deltaRot.mat3[5] + deltaRot.mat3[7]; if (maxDiag === diag[0]) { a = Math.sqrt((diag[0] - cosTheta) * invOneMinusCosTheta); // not zero. invOneMinusCosTheta /= a; b = 0.5 * offDiagSum13 * invOneMinusCosTheta; c = 0.5 * offDiagSum26 * invOneMinusCosTheta; sinTheta = 0.5 * (deltaRot.mat3[7] - deltaRot.mat3[5]) / a; } else if (maxDiag === diag[1]) { b = Math.sqrt((diag[1] - cosTheta) * invOneMinusCosTheta); // not zero. invOneMinusCosTheta /= b; c = 0.5 * offDiagSum57 * invOneMinusCosTheta; a = 0.5 * offDiagSum13 * invOneMinusCosTheta; sinTheta = 0.5 * (deltaRot.mat3[2] - deltaRot.mat3[6]) / b; } else { c = Math.sqrt((diag[2] - cosTheta) * invOneMinusCosTheta); // not zero. invOneMinusCosTheta /= c; a = 0.5 * offDiagSum26 * invOneMinusCosTheta; b = 0.5 * offDiagSum57 * invOneMinusCosTheta; sinTheta = 0.5 * (deltaRot.mat3[3] - deltaRot.mat3[1]) / c; } // Constructs a new matrix after interpolating the angles. // Multiplying mat0 by the first matrix yields mat1, but by creating a state // in the middle of that matrix, you can obtain a matrix that is // an intermediate state between mat0 and mat1. const angle = amt * Math.atan2(sinTheta, cosTheta); const cosAngle = Math.cos(angle); const sinAngle = Math.sin(angle); const oneMinusCosAngle = 1 - cosAngle; const ab = a * b; const bc = b * c; const ca = c * a; // 3x3 const lerpedRotMat = new Matrix( [ cosAngle + oneMinusCosAngle * a * a, oneMinusCosAngle * ab + sinAngle * c, oneMinusCosAngle * ca - sinAngle * b, oneMinusCosAngle * ab - sinAngle * c, cosAngle + oneMinusCosAngle * b * b, oneMinusCosAngle * bc + sinAngle * a, oneMinusCosAngle * ca + sinAngle * b, oneMinusCosAngle * bc - sinAngle * a, cosAngle + oneMinusCosAngle * c * c ]); // Multiply this to mat0 from left to get the interpolated front vector. // calculate newEye, newCenter with newFront vector. lerpedRotMat.multiplyVec(front0, newFront); // this is vec3 newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium); newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium); lerpedRotMat.multiplyVec(up0, newUp); // this is vec3 // We also get the up vector in the same way and set the camera. // The eye position and center position are calculated based on the front vector. this.camera( newEye.x, newEye.y, newEye.z, newCenter.x, newCenter.y, newCenter.z, newUp.x, newUp.y, newUp.z ); } //////////////////////////////////////////////////////////////////////////////// // Camera Helper Methods //////////////////////////////////////////////////////////////////////////////// // @TODO: combine this function with _setDefaultCamera to compute these values // as-needed _computeCameraDefaultSettings() { this.defaultAspectRatio = this._renderer.width / this._renderer.height; this.defaultEyeX = 0; this.defaultEyeY = 0; this.defaultEyeZ = 800; this.defaultCameraFOV = 2 * Math.atan(this._renderer.height / 2 / this.defaultEyeZ); this.defaultCenterX = 0; this.defaultCenterY = 0; this.defaultCenterZ = 0; this.defaultCameraNear = this.defaultEyeZ * this._renderer.defaultNearScale(); this.defaultCameraFar = this.defaultEyeZ * this._renderer.defaultFarScale(); } //detect if user didn't set the camera //then call this function below _setDefaultCamera() { this.cameraFOV = this.defaultCameraFOV; this.aspectRatio = this.defaultAspectRatio; this.eyeX = this.defaultEyeX; this.eyeY = this.defaultEyeY; this.eyeZ = this.defaultEyeZ; this.centerX = this.defaultCenterX; this.centerY = this.defaultCenterY; this.centerZ = this.defaultCenterZ; this.upX = 0; this.upY = 1; this.upZ = 0; this.cameraNear = this.defaultCameraNear; this.cameraFar = this.defaultCameraFar; this.perspective(); this.camera(); this.cameraType = 'default'; } _resize() { // If we're using the default camera, update the aspect ratio if (this.cameraType === 'default') { this._computeCameraDefaultSettings(); this.cameraFOV = this.defaultCameraFOV; this.aspectRatio = this.defaultAspectRatio; this.perspective(); } } /** * Returns a copy of a camera. * @private */ copy() { const _cam = new Camera(this._renderer); _cam.cameraFOV = this.cameraFOV; _cam.aspectRatio = this.aspectRatio; _cam.eyeX = this.eyeX; _cam.eyeY = this.eyeY; _cam.eyeZ = this.eyeZ; _cam.centerX = this.centerX; _cam.centerY = this.centerY; _cam.centerZ = this.centerZ; _cam.upX = this.upX; _cam.upY = this.upY; _cam.upZ = this.upZ; _cam.cameraNear = this.cameraNear; _cam.cameraFar = this.cameraFar; _cam.cameraType = this.cameraType; _cam.useLinePerspective = this.useLinePerspective; _cam.cameraMatrix = this.cameraMatrix.copy(); _cam.projMatrix = this.projMatrix.copy(); _cam.yScale = this.yScale; _cam.cameraType = this.cameraType; return _cam; } clone() { return this.copy(); } /** * Returns a camera's local axes: left-right, up-down, and forward-backward, * as defined by vectors in world-space. * @private */ _getLocalAxes() { // calculate camera local Z vector let z0 = this.eyeX - this.centerX; let z1 = this.eyeY - this.centerY; let z2 = this.eyeZ - this.centerZ; // normalize camera local Z vector const eyeDist = Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); if (eyeDist !== 0) { z0 /= eyeDist; z1 /= eyeDist; z2 /= eyeDist; } // calculate camera Y vector let y0 = this.upX; let y1 = this.upY; let y2 = this.upZ; // compute camera local X vector as up vector (local Y) cross local Z let x0 = y1 * z2 - y2 * z1; let x1 = -y0 * z2 + y2 * z0; let x2 = y0 * z1 - y1 * z0; // recompute y = z cross x y0 = z1 * x2 - z2 * x1; y1 = -z0 * x2 + z2 * x0; y2 = z0 * x1 - z1 * x0; // cross product gives area of parallelogram, which is < 1.0 for // non-perpendicular unit-length vectors; so normalize x, y here: const xmag = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); if (xmag !== 0) { x0 /= xmag; x1 /= xmag; x2 /= xmag; } const ymag = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); if (ymag !== 0) { y0 /= ymag; y1 /= ymag; y2 /= ymag; } return { x: [x0, x1, x2], y: [y0, y1, y2], z: [z0, z1, z2] }; } /** * Orbits the camera about center point. For use with orbitControl(). * @private * @param {Number} dTheta change in spherical coordinate theta * @param {Number} dPhi change in spherical coordinate phi * @param {Number} dRadius change in radius */ _orbit(dTheta, dPhi, dRadius) { // Calculate the vector and its magnitude from the center to the viewpoint const diffX = this.eyeX - this.centerX; const diffY = this.eyeY - this.centerY; const diffZ = this.eyeZ - this.centerZ; let camRadius = Math.hypot(diffX, diffY, diffZ); // front vector. unit vector from center to eye. const front = new Vector(diffX, diffY, diffZ).normalize(); // up vector. normalized camera's up vector. const up = new Vector(this.upX, this.upY, this.upZ).normalize(); // y-axis // side vector. Right when viewed from the front const side = Vector.cross(up, front).normalize(); // x-axis // vertical vector. normalized vector of projection of front vector. const vertical = Vector.cross(side, up); // z-axis // update camRadius camRadius *= Math.pow(10, dRadius); // prevent zooming through the center: if (camRadius < this.cameraNear) { camRadius = this.cameraNear; } if (camRadius > this.cameraFar) { camRadius = this.cameraFar; } // calculate updated camera angle // Find the angle between the "up" and the "front", add dPhi to that. // angleBetween() may return negative value. Since this specification is subject to change // due to version updates, it cannot be adopted, so here we calculate using a method // that directly obtains the absolute value. const camPhi = Math.acos(Math.max(-1, Math.min(1, Vector.dot(front, up)))) + dPhi; // Rotate by dTheta in the shortest direction from "vertical" to "side" const camTheta = dTheta; // Invert camera's upX, upY, upZ if dPhi is below 0 or above PI if (camPhi <= 0 || camPhi >= Math.PI) { this.upX *= -1; this.upY *= -1; this.upZ *= -1; } // update eye vector by calculate new front vector up.mult(Math.cos(camPhi)); vertical.mult(Math.cos(camTheta) * Math.sin(camPhi)); side.mult(Math.sin(camTheta) * Math.sin(camPhi)); front.set(up).add(vertical).add(side); this.eyeX = camRadius * front.x + this.centerX; this.eyeY = camRadius * front.y + this.centerY; this.eyeZ = camRadius * front.z + this.centerZ; // update camera this.camera( this.eyeX, this.eyeY, this.eyeZ, this.centerX, this.centerY, this.centerZ, this.upX, this.upY, this.upZ ); } /** * Orbits the camera about center point. For use with orbitControl(). * Unlike _orbit(), the direction of rotation always matches the direction of pointer movement. * @private * @param {Number} dx the x component of the rotation vector. * @param {Number} dy the y component of the rotation vector. * @param {Number} dRadius change in radius */ _orbitFree(dx, dy, dRadius) { // Calculate the vector and its magnitude from the center to the viewpoint const diffX = this.eyeX - this.centerX; const diffY = this.eyeY - this.centerY; const diffZ = this.eyeZ - this.centerZ; let camRadius = Math.hypot(diffX, diffY, diffZ); // front vector. unit vector from center to eye. const front = new Vector(diffX, diffY, diffZ).normalize(); // up vector. camera's up vector. const up = new Vector(this.upX, this.upY, this.upZ); // side vector. Right when viewed from the front. (like x-axis) const side = Vector.cross(up, front).normalize(); // down vector. Bottom when viewed from the front. (like y-axis) const down = Vector.cross(front, side); // side vector and down vector are no longer used as-is. // Create a vector representing the direction of rotation // in the form cos(direction)*side + sin(direction)*down. // Make the current side vector into this. const directionAngle = Math.atan2(dy, dx); down.mult(Math.sin(directionAngle)); side.mult(Math.cos(directionAngle)).add(down); // The amount of rotation is the size of the vector (dx, dy). const rotAngle = Math.sqrt(dx * dx + dy * dy); // The vector that is orthogonal to both the front vector and // the rotation direction vector is the rotation axis vector. const axis = Vector.cross(front, side); // update camRadius camRadius *= Math.pow(10, dRadius); // prevent zooming through the center: if (camRadius < this.cameraNear) { camRadius = this.cameraNear; } if (camRadius > this.cameraFar) { camRadius = this.cameraFar; } // If the axis vector is likened to the z-axis, the front vector is // the x-axis and the side vector is the y-axis. Rotate the up and front // vectors respectively by thinking of them as rotations around the z-axis. // Calculate the components by taking the dot product and // calculate a rotation based on that. const c = Math.cos(rotAngle); const s = Math.sin(rotAngle); const dotFront = up.dot(front); const dotSide = up.dot(side); const ux = dotFront * c + dotSide * s; const uy = -dotFront * s + dotSide * c; const uz = up.dot(axis); up.x = ux * front.x + uy * side.x + uz * axis.x; up.y = ux * front.y + uy * side.y + uz * axis.y; up.z = ux * front.z + uy * side.z + uz * axis.z; // We won't be using the side vector and the front vector anymore, // so let's make the front vector into the vector from the center to the new eye. side.mult(-s); front.mult(c).add(side).mult(camRadius); // it's complete. let's update camera. this.camera( front.x + this.centerX, front.y + this.centerY, front.z + this.centerZ, this.centerX, this.centerY, this.centerZ, up.x, up.y, up.z ); } /** * Returns true if camera is currently attached to renderer. * @private */ _isActive() { return this === this._renderer.states.curCamera; } } function camera(p5, fn){ //////////////////////////////////////////////////////////////////////////////// // p5.Prototype Methods //////////////////////////////////////////////////////////////////////////////// /** * Sets the position and orientation of the current camera in a 3D sketch. * * `camera()` allows objects to be viewed from different angles. It has nine * parameters that are all optional. * * The first three parameters, `x`, `y`, and `z`, are the coordinates of the * camera’s position. For example, calling `camera(0, 0, 0)` places the camera * at the origin `(0, 0, 0)`. By default, the camera is placed at * `(0, 0, 800)`. * * The next three parameters, `centerX`, `centerY`, and `centerZ` are the * coordinates of the point where the camera faces. For example, calling * `camera(0, 0, 0, 10, 20, 30)` places the camera at the origin `(0, 0, 0)` * and points it at `(10, 20, 30)`. By default, the camera points at the * origin `(0, 0, 0)`. * * The last three parameters, `upX`, `upY`, and `upZ` are the components of * the "up" vector. The "up" vector orients the camera’s y-axis. For example, * calling `camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up" * vector is `(0, 1, 0)`. * * Note: `camera()` can only be used in WebGL mode. * * @method camera * @for p5 * @param {Number} [x] x-coordinate of the camera. Defaults to 0. * @param {Number} [y] y-coordinate of the camera. Defaults to 0. * @param {Number} [z] z-coordinate of the camera. Defaults to 800. * @param {Number} [centerX] x-coordinate of the point the camera faces. Defaults to 0. * @param {Number} [centerY] y-coordinate of the point the camera faces. Defaults to 0. * @param {Number} [centerZ] z-coordinate of the point the camera faces. Defaults to 0. * @param {Number} [upX] x-component of the camera’s "up" vector. Defaults to 0. * @param {Number} [upY] y-component of the camera’s "up" vector. Defaults to 1. * @param {Number} [upZ] z-component of the camera’s "up" vector. Defaults to 0. * @chainable * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Move the camera to the top-right. * camera(200, -400, 800); * * // Draw the box. * box(); * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube apperas to sway left and right on a gray background.'); * } * * function draw() { * background(200); * * // Calculate the camera's x-coordinate. * let x = 400 * cos(frameCount * 0.01); * * // Orbit the camera around the box. * camera(x, -400, 800); * * // Draw the box. * box(); * } * * @example * // Adjust the range sliders to change the camera's position. * * let xSlider; * let ySlider; * let zSlider; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create slider objects to set the camera's coordinates. * xSlider = createSlider(-400, 400, 400); * xSlider.position(0, 100); * xSlider.size(100); * ySlider = createSlider(-400, 400, -200); * ySlider.position(0, 120); * ySlider.size(100); * zSlider = createSlider(0, 1600, 800); * zSlider.position(0, 140); * zSlider.size(100); * * describe( * 'A white cube drawn against a gray background. Three range sliders appear beneath the image. The camera position changes when the user moves the sliders.' * ); * } * * function draw() { * background(200); * * // Get the camera's coordinates from the sliders. * let x = xSlider.value(); * let y = ySlider.value(); * let z = zSlider.value(); * * // Move the camera. * camera(x, y, z); * * // Draw the box. * box(); * } */ fn.camera = function (...args) { this._assert3d('camera'); // p5._validateParameters('camera', args); this._renderer.camera(...args); return this; }; /** * Sets a perspective projection for the current camera in a 3D sketch. * * In a perspective projection, shapes that are further from the camera appear * smaller than shapes that are near the camera. This technique, called * foreshortening, creates realistic 3D scenes. It’s applied by default in * WebGL mode. * * `perspective()` changes the camera’s perspective by changing its viewing * frustum. The frustum is the volume of space that’s visible to the camera. * Its shape is a pyramid with its top cut off. The camera is placed where * the top of the pyramid should be and views everything between the frustum’s * top (near) plane and its bottom (far) plane. * * The first parameter, `fovy`, is the camera’s vertical field of view. It’s * an angle that describes how tall or narrow a view the camera has. For * example, calling `perspective(0.5)` sets the camera’s vertical field of * view to 0.5 radians. By default, `fovy` is calculated based on the sketch’s * height and the camera’s default z-coordinate, which is 800. The formula for * the default `fovy` is `2 * atan(height / 2 / 800)`. * * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number * that describes the ratio of the top plane’s width to its height. For * example, calling `perspective(0.5, 1.5)` sets the camera’s field of view to * 0.5 radians and aspect ratio to 1.5, which would make shapes appear thinner * on a square canvas. By default, aspect is set to `width / height`. * * The third parameter, `near`, is the distance from the camera to the near * plane. For example, calling `perspective(0.5, 1.5, 100)` sets the camera’s * field of view to 0.5 radians, its aspect ratio to 1.5, and places the near * plane 100 pixels from the camera. Any shapes drawn less than 100 pixels * from the camera won’t be visible. By default, near is set to `0.1 * 800`, * which is 1/10th the default distance between the camera and the origin. * * The fourth parameter, `far`, is the distance from the camera to the far * plane. For example, calling `perspective(0.5, 1.5, 100, 10000)` sets the * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, places the * near plane 100 pixels from the camera, and places the far plane 10,000 * pixels from the camera. Any shapes drawn more than 10,000 pixels from the * camera won’t be visible. By default, far is set to `10 * 800`, which is 10 * times the default distance between the camera and the origin. * * Note: `perspective()` can only be used in WebGL mode. * * @method perspective * @for p5 * @param {Number} [fovy] camera frustum vertical field of view. Defaults to * `2 * atan(height / 2 / 800)`. * @param {Number} [aspect] camera frustum aspect ratio. Defaults to * `width / height`. * @param {Number} [near] distance from the camera to the near clipping plane. * Defaults to `0.1 * 800`. * @param {Number} [far] distance from the camera to the far clipping plane. * Defaults to `10 * 800`. * @chainable * * @example * // Double-click to squeeze the box. * * let isSqueezed = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white rectangular prism on a gray background. The box appears to become thinner when the user double-clicks.'); * } * * function draw() { * background(200); * * // Place the camera at the top-right. * camera(400, -400, 800); * * if (isSqueezed === true) { * // Set fovy to 0.2. * // Set aspect to 1.5. * perspective(0.2, 1.5); * } * * // Draw the box. * box(); * } * * // Change the camera's perspective when the user double-clicks. * function doubleClicked() { * isSqueezed = true; * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white rectangular prism on a gray background. The prism moves away from the camera until it disappears.'); * } * * function draw() { * background(200); * * // Place the camera at the top-right. * camera(400, -400, 800); * * // Set fovy to 0.2. * // Set aspect to 1.5. * // Set near to 600. * // Set far to 1200. * perspective(0.2, 1.5, 600, 1200); * * // Move the origin away from the camera. * let x = -frameCount; * let y = frameCount; * let z = -2 * frameCount; * translate(x, y, z); * * // Draw the box. * box(); * } */ fn.perspective = function (...args) { this._assert3d('perspective'); // p5._validateParameters('perspective', args); this._renderer.perspective(...args); return this; }; /** * Enables or disables perspective for lines in 3D sketches. * * In WebGL mode, lines can be drawn with a thinner stroke when they’re * further from the camera. Doing so gives them a more realistic appearance. * * By default, lines are drawn differently based on the type of perspective * being used: * - `perspective()` and `frustum()` simulate a realistic perspective. In * these modes, stroke weight is affected by the line’s distance from the * camera. Doing so results in a more natural appearance. `perspective()` is * the default mode for 3D sketches. * - `ortho()` doesn’t simulate a realistic perspective. In this mode, stroke * weights are consistent regardless of the line’s distance from the camera. * Doing so results in a more predictable and consistent appearance. * * `linePerspective()` can override the default line drawing mode. * * The parameter, `enable`, is optional. It’s a `Boolean` value that sets the * way lines are drawn. If `true` is passed, as in `linePerspective(true)`, * then lines will appear thinner when they are further from the camera. If * `false` is passed, as in `linePerspective(false)`, then lines will have * consistent stroke weights regardless of their distance from the camera. By * default, `linePerspective()` is enabled. * * Calling `linePerspective()` without passing an argument returns `true` if * it's enabled and `false` if not. * * Note: `linePerspective()` can only be used in WebGL mode. * * @method linePerspective * @for p5 * @param {Boolean} enable whether to enable line perspective. * * @example * // Double-click the canvas to toggle the line perspective. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A white cube with black edges on a gray background. Its edges toggle between thick and thin when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Translate the origin toward the camera. * translate(-10, 10, 600); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } * * // Toggle the line perspective when the user double-clicks. * function doubleClicked() { * let isEnabled = linePerspective(); * linePerspective(!isEnabled); * } * * @example * // Double-click the canvas to toggle the line perspective. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A row of cubes with black edges on a gray background. Their edges toggle between thick and thin when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Use an orthographic projection. * ortho(); * * // Translate the origin toward the camera. * translate(-10, 10, 600); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } * * // Toggle the line perspective when the user double-clicks. * function doubleClicked() { * let isEnabled = linePerspective(); * linePerspective(!isEnabled); * } */ /** * @method linePerspective * @return {boolean} whether line perspective is enabled. */ fn.linePerspective = function (enable) { // p5._validateParameters('linePerspective', arguments); if (!(this._renderer instanceof Renderer3D)) { throw new Error('linePerspective() must be called in WebGL mode.'); } return this._renderer.linePerspective(enable); }; /** * Sets an orthographic projection for the current camera in a 3D sketch. * * In an orthographic projection, shapes with the same size always appear the * same size, regardless of whether they are near or far from the camera. * * `ortho()` changes the camera’s perspective by changing its viewing frustum * from a truncated pyramid to a rectangular prism. The camera is placed in * front of the frustum and views everything between the frustum’s near plane * and its far plane. `ortho()` has six optional parameters to define the * frustum. * * The first four parameters, `left`, `right`, `bottom`, and `top`, set the * coordinates of the frustum’s sides, bottom, and top. For example, calling * `ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide and * 400 pixels tall. By default, these coordinates are set based on the * sketch’s width and height, as in * `ortho(-width / 2, width / 2, -height / 2, height / 2)`. * * The last two parameters, `near` and `far`, set the distance of the * frustum’s near and far plane from the camera. For example, calling * `ortho(-100, 100, 200, 200, 50, 1000)` creates a frustum that’s 200 pixels * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000 * pixels from the camera. By default, `near` and `far` are set to 0 and * `max(width, height) + 800`, respectively. * * Note: `ortho()` can only be used in WebGL mode. * * @method ortho * @for p5 * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 2`. * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 2`. * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`. * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 2`. * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to 0. * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`. * @chainable * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A row of tiny, white cubes on a gray background. All the cubes appear the same size.'); * } * * function draw() { * background(200); * * // Apply an orthographic projection. * ortho(); * * // Translate the origin toward the camera. * translate(-10, 10, 600); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Apply an orthographic projection. * // Center the frustum. * // Set its width and height to 20. * // Place its near plane 300 pixels from the camera. * // Place its far plane 350 pixels from the camera. * ortho(-10, 10, -10, 10, 300, 350); * * // Translate the origin toward the camera. * translate(-10, 10, 600); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } */ fn.ortho = function (...args) { this._assert3d('ortho'); // p5._validateParameters('ortho', args); this._renderer.ortho(...args); return this; }; /** * Sets the frustum of the current camera in a 3D sketch. * * In a frustum projection, shapes that are further from the camera appear * smaller than shapes that are near the camera. This technique, called * foreshortening, creates realistic 3D scenes. * * `frustum()` changes the default camera’s perspective by changing its * viewing frustum. The frustum is the volume of space that’s visible to the * camera. The frustum’s shape is a pyramid with its top cut off. The camera * is placed where the top of the pyramid should be and points towards the * base of the pyramid. It views everything within the frustum. * * The first four parameters, `left`, `right`, `bottom`, and `top`, set the * coordinates of the frustum’s sides, bottom, and top. For example, calling * `frustum(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide * and 400 pixels tall. By default, these coordinates are set based on the * sketch’s width and height, as in * `ortho(-width / 20, width / 20, height / 20, -height / 20)`. * * The last two parameters, `near` and `far`, set the distance of the * frustum’s near and far plane from the camera. For example, calling * `ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s 200 pixels * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000 * pixels from the camera. By default, near is set to `0.1 * 800`, which is * 1/10th the default distance between the camera and the origin. `far` is set * to `10 * 800`, which is 10 times the default distance between the camera * and the origin. * * Note: `frustum()` can only be used in WebGL mode. * * @method frustum * @for p5 * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 20`. * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 20`. * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`. * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 20`. * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`. * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `10 * 800`. * @chainable * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A row of white cubes on a gray background.'); * } * * function draw() { * background(200); * * // Apply the default frustum projection. * frustum(); * * // Translate the origin toward the camera. * translate(-10, 10, 600); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } * * @example * function setup() { * createCanvas(100, 100, WEBGL); * describe('A white cube on a gray background.'); * } * * function draw() { * background(200); * * // Adjust the frustum. * // Center it. * // Set its width and height to 20 pixels. * // Place its near plane 300 pixels from the camera. * // Place its far plane 350 pixels from the camera. * frustum(-10, 10, -10, 10, 300, 350); * * // Translate the origin toward the camera. * translate(-10, 10, 600); * * // Rotate the coordinate system. * rotateY(-0.1); * rotateX(-0.1); * * // Draw the row of boxes. * for (let i = 0; i < 6; i += 1) { * translate(0, 0, -40); * box(10); * } * } */ fn.frustum = function (...args) { this._assert3d('frustum'); // p5._validateParameters('frustum', args); this._renderer.frustum(...args); return this; }; /** * Creates a new p5.Camera object. * * The new camera is initialized with a default position `(0, 0, 800)` and a * default perspective projection. Its properties can be controlled with * p5.Camera methods such as * `myCamera.lookAt(0, 0, 0)`. * * Note: Every 3D sketch starts with a default camera initialized. * This camera can be controlled with the functions * camera(), * perspective(), * ortho(), and * frustum() if it's the only camera in the scene. * * Note: `createCamera()` can only be used in WebGL mode. * * @method createCamera * @return {p5.Camera} the new camera. * @for p5 * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let usingCam1 = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * // Place it at the top-left. * // Point it at the origin. * cam2 = createCamera(); * cam2.setPosition(400, -400, 800); * cam2.lookAt(0, 0, 0); * * // Set the current camera to cam1. * setCamera(cam1); * * describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'); * } * * function draw() { * background(200); * * // Draw the box. * box(); * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (usingCam1 === true) { * setCamera(cam2); * usingCam1 = false; * } else { * setCamera(cam1); * usingCam1 = true; * } * } */ fn.createCamera = function () { this._assert3d('createCamera'); return this._renderer.createCamera(); }; /** * Sets the current (active) camera of a 3D sketch. * * `setCamera()` allows for switching between multiple cameras created with * createCamera(). * * Note: `setCamera()` can only be used in WebGL mode. * * @method setCamera * @param {p5.Camera} cam camera that should be made active. * @for p5 * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let usingCam1 = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * // Place it at the top-left. * // Point it at the origin. * cam2 = createCamera(); * cam2.setPosition(400, -400, 800); * cam2.lookAt(0, 0, 0); * * // Set the current camera to cam1. * setCamera(cam1); * * describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'); * } * * function draw() { * background(200); * * // Draw the box. * box(); * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (usingCam1 === true) { * setCamera(cam2); * usingCam1 = false; * } else { * setCamera(cam1); * usingCam1 = true; * } * } */ fn.setCamera = function (cam) { this._renderer.setCamera(cam); }; /** * A class to describe a camera for viewing a 3D sketch. * * Each `p5.Camera` object represents a camera that views a section of 3D * space. It stores information about the camera’s position, orientation, and * projection. * * In WebGL mode, the default camera is a `p5.Camera` object that can be * controlled with the camera(), * perspective(), * ortho(), and * frustum() functions. Additional cameras can be * created with createCamera() and activated * with setCamera(). * * Note: `p5.Camera`’s methods operate in two coordinate systems: * - The “world” coordinate system describes positions in terms of their * relationship to the origin along the x-, y-, and z-axes. For example, * calling `myCamera.setPosition()` places the camera in 3D space using * "world" coordinates. * - The "local" coordinate system describes positions from the camera's point * of view: left-right, up-down, and forward-backward. For example, calling * `myCamera.move()` moves the camera along its own axes. * * @class p5.Camera * @constructor * @param {RendererGL} rendererGL instance of WebGL renderer * * @example * let cam; * let delta = 0.001; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.' * ); * } * * function draw() { * background(200); * * // Turn the camera left and right, called "panning". * cam.pan(delta); * * // Switch directions every 120 frames. * if (frameCount % 120 === 0) { * delta *= -1; * } * * // Draw the box. * box(); * } * * @example * // Double-click to toggle between cameras. * * let cam1; * let cam2; * let isDefaultCamera = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the first camera. * // Keep its default settings. * cam1 = createCamera(); * * // Create the second camera. * // Place it at the top-left. * // Point it at the origin. * cam2 = createCamera(); * cam2.setPosition(400, -400, 800); * cam2.lookAt(0, 0, 0); * * // Set the current camera to cam1. * setCamera(cam1); * * describe( * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Draw the box. * box(); * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (isDefaultCamera === true) { * setCamera(cam2); * isDefaultCamera = false; * } else { * setCamera(cam1); * isDefaultCamera = true; * } * } */ p5.Camera = Camera; Renderer3D.prototype.camera = function(...args) { this.states.setValue('curCamera', this.states.curCamera.clone()); this.states.curCamera.camera(...args); }; Renderer3D.prototype.perspective = function(...args) { this.states.setValue('curCamera', this.states.curCamera.clone()); this.states.curCamera.perspective(...args); }; Renderer3D.prototype.linePerspective = function(enable) { if (enable !== undefined) { this.states.setValue('curCamera', this.states.curCamera.clone()); // Set the line perspective if enable is provided this.states.curCamera.useLinePerspective = enable; } else { // If no argument is provided, return the current value return this.states.curCamera.useLinePerspective; } }; Renderer3D.prototype.ortho = function(...args) { this.states.setValue('curCamera', this.states.curCamera.clone()); this.states.curCamera.ortho(...args); }; Renderer3D.prototype.frustum = function(...args) { this.states.setValue('curCamera', this.states.curCamera.clone()); this.states.curCamera.frustum(...args); }; Renderer3D.prototype.createCamera = function() { // compute default camera settings, then set a default camera const _cam = new Camera(this); _cam._computeCameraDefaultSettings(); _cam._setDefaultCamera(); return _cam; }; Renderer3D.prototype.setCamera = function(cam) { this.states.setValue('curCamera', cam); // set the projection matrix (which is not normally updated each frame) this.states.setValue('uPMatrix', this.states.uPMatrix.clone()); this.states.uPMatrix.set(cam.projMatrix); this.states.setValue('uViewMatrix', this.states.uViewMatrix.clone()); this.states.uViewMatrix.set(cam.cameraMatrix); }; /** * The camera’s x-coordinate. * * By default, the camera’s x-coordinate is set to 0 in "world" space. * * @property {Number} eyeX * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The text "eyeX: 0" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of eyeX, rounded to the nearest integer. * text(`eyeX: ${round(cam.eyeX)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The cube appears to move left and right as the camera moves. The text "eyeX: X" is written in black beneath the cube. X oscillates between -25 and 25.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the new x-coordinate. * let x = 25 * sin(frameCount * 0.01); * * // Set the camera's position. * cam.setPosition(x, -400, 800); * * // Display the value of eyeX, rounded to the nearest integer. * text(`eyeX: ${round(cam.eyeX)}`, 0, 45); * } */ /** * The camera’s y-coordinate. * * By default, the camera’s y-coordinate is set to 0 in "world" space. * * @property {Number} eyeY * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * // Set the camera. * setCamera(cam); * * describe( * 'A white cube on a gray background. The text "eyeY: -400" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of eyeY, rounded to the nearest integer. * text(`eyeY: ${round(cam.eyeY)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The cube appears to move up and down as the camera moves. The text "eyeY: Y" is written in black beneath the cube. Y oscillates between -374 and -425.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the new y-coordinate. * let y = 25 * sin(frameCount * 0.01) - 400; * * // Set the camera's position. * cam.setPosition(0, y, 800); * * // Display the value of eyeY, rounded to the nearest integer. * text(`eyeY: ${round(cam.eyeY)}`, 0, 45); * } */ /** * The camera’s z-coordinate. * * By default, the camera’s z-coordinate is set to 800 in "world" space. * * @property {Number} eyeZ * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The text "eyeZ: 800" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of eyeZ, rounded to the nearest integer. * text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at the origin. * cam.lookAt(0, 0, 0); * * describe( * 'A white cube on a gray background. The cube appears to move forward and back as the camera moves. The text "eyeZ: Z" is written in black beneath the cube. Z oscillates between 700 and 900.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the new z-coordinate. * let z = 100 * sin(frameCount * 0.01) + 800; * * // Set the camera's position. * cam.setPosition(0, -400, z); * * // Display the value of eyeZ, rounded to the nearest integer. * text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45); * } */ /** * The x-coordinate of the place where the camera looks. * * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so * `myCamera.centerX` is 0. * * @property {Number} centerX * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at (10, 20, -30). * cam.lookAt(10, 20, -30); * * describe( * 'A white cube on a gray background. The text "centerX: 10" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of centerX, rounded to the nearest integer. * text(`centerX: ${round(cam.centerX)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right. * cam.setPosition(100, -400, 800); * * // Point the camera at (10, 20, -30). * cam.lookAt(10, 20, -30); * * describe( * 'A white cube on a gray background. The cube appears to move left and right as the camera shifts its focus. The text "centerX: X" is written in black beneath the cube. X oscillates between -15 and 35.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the new x-coordinate. * let x = 25 * sin(frameCount * 0.01) + 10; * * // Point the camera. * cam.lookAt(x, 20, -30); * * // Display the value of centerX, rounded to the nearest integer. * text(`centerX: ${round(cam.centerX)}`, 0, 45); * } */ /** * The y-coordinate of the place where the camera looks. * * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so * `myCamera.centerY` is 0. * * @property {Number} centerY * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at (10, 20, -30). * cam.lookAt(10, 20, -30); * * describe( * 'A white cube on a gray background. The text "centerY: 20" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of centerY, rounded to the nearest integer. * text(`centerY: ${round(cam.centerY)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right. * cam.setPosition(100, -400, 800); * * // Point the camera at (10, 20, -30). * cam.lookAt(10, 20, -30); * * describe( * 'A white cube on a gray background. The cube appears to move up and down as the camera shifts its focus. The text "centerY: Y" is written in black beneath the cube. Y oscillates between -5 and 45.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the new y-coordinate. * let y = 25 * sin(frameCount * 0.01) + 20; * * // Point the camera. * cam.lookAt(10, y, -30); * * // Display the value of centerY, rounded to the nearest integer. * text(`centerY: ${round(cam.centerY)}`, 0, 45); * } */ /** * The y-coordinate of the place where the camera looks. * * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so * `myCamera.centerZ` is 0. * * @property {Number} centerZ * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-center. * cam.setPosition(0, -400, 800); * * // Point the camera at (10, 20, -30). * cam.lookAt(10, 20, -30); * * describe( * 'A white cube on a gray background. The text "centerZ: -30" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of centerZ, rounded to the nearest integer. * text(`centerZ: ${round(cam.centerZ)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Place the camera at the top-right. * cam.setPosition(100, -400, 800); * * // Point the camera at (10, 20, -30). * cam.lookAt(10, 20, -30); * * describe( * 'A white cube on a gray background. The cube appears to move forward and back as the camera shifts its focus. The text "centerZ: Z" is written in black beneath the cube. Z oscillates between -55 and -25.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the new z-coordinate. * let z = 25 * sin(frameCount * 0.01) - 30; * * // Point the camera. * cam.lookAt(10, 20, z); * * // Display the value of centerZ, rounded to the nearest integer. * text(`centerZ: ${round(cam.centerZ)}`, 0, 45); * } */ /** * The x-component of the camera's "up" vector. * * The camera's "up" vector orients its y-axis. By default, the "up" vector is * `(0, 1, 0)`, so its x-component is 0 in "local" space. * * @property {Number} upX * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right: (100, -400, 800) * // Point it at the origin: (0, 0, 0) * // Set its "up" vector: (0, 1, 0). * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0); * * describe( * 'A white cube on a gray background. The text "upX: 0" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of upX, rounded to the nearest tenth. * text(`upX: ${round(cam.upX, 1)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right: (100, -400, 800) * // Point it at the origin: (0, 0, 0) * // Set its "up" vector: (0, 1, 0). * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0); * * describe( * 'A white cube on a gray background. The cube appears to rock back and forth. The text "upX: X" is written in black beneath it. X oscillates between -1 and 1.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the x-component. * let x = sin(frameCount * 0.01); * * // Update the camera's "up" vector. * cam.camera(100, -400, 800, 0, 0, 0, x, 1, 0); * * // Display the value of upX, rounded to the nearest tenth. * text(`upX: ${round(cam.upX, 1)}`, 0, 45); * } */ /** * The y-component of the camera's "up" vector. * * The camera's "up" vector orients its y-axis. By default, the "up" vector is * `(0, 1, 0)`, so its y-component is 1 in "local" space. * * @property {Number} upY * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right: (100, -400, 800) * // Point it at the origin: (0, 0, 0) * // Set its "up" vector: (0, 1, 0). * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0); * * describe( * 'A white cube on a gray background. The text "upY: 1" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of upY, rounded to the nearest tenth. * text(`upY: ${round(cam.upY, 1)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right: (100, -400, 800) * // Point it at the origin: (0, 0, 0) * // Set its "up" vector: (0, 1, 0). * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0); * * describe( * 'A white cube on a gray background. The cube flips upside-down periodically. The text "upY: Y" is written in black beneath it. Y oscillates between -1 and 1.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the y-component. * let y = sin(frameCount * 0.01); * * // Update the camera's "up" vector. * cam.camera(100, -400, 800, 0, 0, 0, 0, y, 0); * * // Display the value of upY, rounded to the nearest tenth. * text(`upY: ${round(cam.upY, 1)}`, 0, 45); * } */ /** * The z-component of the camera's "up" vector. * * The camera's "up" vector orients its y-axis. By default, the "up" vector is * `(0, 1, 0)`, so its z-component is 0 in "local" space. * * @property {Number} upZ * @for p5.Camera * @readonly * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right: (100, -400, 800) * // Point it at the origin: (0, 0, 0) * // Set its "up" vector: (0, 1, 0). * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0); * * describe( * 'A white cube on a gray background. The text "upZ: 0" is written in black beneath it.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Display the value of upZ, rounded to the nearest tenth. * text(`upZ: ${round(cam.upZ, 1)}`, 0, 45); * } * * @example * let cam; * let font; * * async function setup() { * // Load a font and create a p5.Font object. * font = await loadFont('assets/inconsolata.otf'); * createCanvas(100, 100, WEBGL); * * // Create a p5.Camera object. * cam = createCamera(); * * // Set the camera * setCamera(cam); * * // Place the camera at the top-right: (100, -400, 800) * // Point it at the origin: (0, 0, 0) * // Set its "up" vector: (0, 1, 0). * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0); * * describe( * 'A white cube on a gray background. The cube appears to rock back and forth. The text "upZ: Z" is written in black beneath it. Z oscillates between -1 and 1.' * ); * } * * function draw() { * background(200); * * // Style the box. * fill(255); * * // Draw the box. * box(); * * // Style the text. * textAlign(CENTER); * textSize(16); * textFont(font); * fill(0); * * // Calculate the z-component. * let z = sin(frameCount * 0.01); * * // Update the camera's "up" vector. * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, z); * * // Display the value of upZ, rounded to the nearest tenth. * text(`upZ: ${round(cam.upZ, 1)}`, 0, 45); * } */ } if(typeof p5 !== 'undefined'){ camera(p5, p5.prototype); } var libtess_min = {exports: {}}; /* Copyright 2000, Silicon Graphics, Inc. All Rights Reserved. Copyright 2015, Google Inc. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice including the dates of first publication and either this permission notice or a reference to http://oss.sgi.com/projects/FreeB/ shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL SILICON GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Original Code. The Original Code is: OpenGL Sample Implementation, Version 1.2.1, released January 26, 2000, developed by Silicon Graphics, Inc. The Original Code is Copyright (c) 1991-2000 Silicon Graphics, Inc. Copyright in any portions created by third parties is as indicated elsewhere herein. All Rights Reserved. */ (function (module) { var n;function t(a,b){return a.b===b.b&&a.a===b.a}function u(a,b){return a.ba?0:a;c=0>c?0:c;return a<=c?0===c?(b+d)/2:b+a/(a+c)*(d-b):d+c/(a+c)*(b-d)}function ea(a){var b=B(a.b);C(b,a.c);C(b.b,a.c);D(b,a.a);return b}function E(a,b){var c=false,d=false;a!==b&&(b.a!==a.a&&(d=true,F(b.a,a.a)),b.d!==a.d&&(c=true,G(b.d,a.d)),H(b,a),d||(C(b,a.a),a.a.c=a),c||(D(b,a.d),a.d.a=a));}function I(a){var b=a.b,c=false;a.d!==a.b.d&&(c=true,G(a.d,a.b.d));a.c===a?F(a.a,null):(a.b.d.a=J(a),a.a.c=a.c,H(a,J(a)),c||D(a,a.d));b.c===b?(F(b.a,null),G(b.d,null)):(a.d.a=J(b),b.a.c=b.c,H(b,J(b)));fa(a);} function K(a){var b=B(a),c=b.b;H(b,a.e);b.a=a.b.a;C(c,b.a);b.d=c.d=a.d;b=b.b;H(a.b,J(a.b));H(a.b,b);a.b.a=b.a;b.b.a.c=b.b;b.b.d=a.b.d;b.f=a.f;b.b.f=a.b.f;return b}function L(a,b){var c=false,d=B(a),e=d.b;b.d!==a.d&&(c=true,G(b.d,a.d));H(d,a.e);H(e,b);d.a=a.b.a;e.a=b.a;d.d=e.d=a.d;a.d.a=e;c||D(d,a.d);return d}function B(a){var b=new M,c=new M,d=a.b.h;c.h=d;d.b.h=b;b.h=a;a.b.h=c;b.b=c;b.c=b;b.e=c;c.b=b;c.c=c;return c.e=b}function H(a,b){var c=a.c,d=b.c;c.b.e=b;d.b.e=a;a.c=d;b.c=c;} function C(a,b){var c=b.f,d=new N(b,c);c.e=d;b.f=d;c=d.c=a;do c.a=d,c=c.c;while(c!==a)}function D(a,b){var c=b.d,d=new ga(b,c);c.b=d;b.d=d;d.a=a;d.c=b.c;c=a;do c.d=d,c=c.e;while(c!==a)}function fa(a){var b=a.h;a=a.b.h;b.b.h=a;a.b.h=b;}function F(a,b){var c=a.c,d=c;do d.a=b,d=d.c;while(d!==c);c=a.f;d=a.e;d.f=c;c.e=d;}function G(a,b){var c=a.a,d=c;do d.d=b,d=d.e;while(d!==c);c=a.d;d=a.b;d.d=c;c.b=d;}function ha(a){var b=0;Math.abs(a[1])>Math.abs(a[0])&&(b=1);Math.abs(a[2])>Math.abs(a[b])&&(b=2);return b}var O=4*1E150;function P(a,b){a.f+=b.f;a.b.f+=b.b.f;}function ia(a,b,c){a=a.a;b=b.a;c=c.a;if(b.b.a===a)return c.b.a===a?u(b.a,c.a)?0>=x(c.b.a,b.a,c.a):0<=x(b.b.a,c.a,b.a):0>=x(c.b.a,a,c.a);if(c.b.a===a)return 0<=x(b.b.a,a,b.a);b=v(b.b.a,a,b.a);a=v(c.b.a,a,c.a);return b>=a}function Q(a){a.a.i=null;var b=a.e;b.a.c=b.c;b.c.a=b.a;a.e=null;}function ja(a,b){I(a.a);a.c=false;a.a=b;b.i=a;}function ka(a){var b=a.a.a;do a=R(a);while(a.a.a===b);a.c&&(b=L(S(a).a.b,a.a.e),ja(a,b),a=R(a));return a} function la(a,b,c){var d=new ma;d.a=c;d.e=na(a.f,b.e,d);return c.i=d}function oa(a,b){switch(a.s){case 100130:return 0!==(b&1);case 100131:return 0!==b;case 100132:return 0b;case 100134:return 2<=b||-2>=b}return false}function pa(a){var b=a.a,c=b.d;c.c=a.d;c.a=b;Q(a);}function T(a,b,c){a=b;for(b=b.a;a!==c;){a.c=false;var d=S(a),e=d.a;if(e.a!==b.a){if(!d.c){pa(a);break}e=L(b.c.b,e.b);ja(d,e);}b.c!==e&&(E(J(e),e),E(b,e));pa(a);b=d.a;a=d;}return b} function U(a,b,c,d,e,f){var g=true;do la(a,b,c.b),c=c.c;while(c!==d);for(null===e&&(e=S(b).a.b.c);;){d=S(b);c=d.a.b;if(c.a!==e.a)break;c.c!==e&&(E(J(c),c),E(J(e),c));d.f=b.f-c.f;d.d=oa(a,d.f);b.b=true;!g&&qa(a,b)&&(P(c,e),Q(b),I(e));g=false;b=d;e=c;}b.b=true;f&&ra(a,b);}function sa(a,b,c,d,e){var f=[b.g[0],b.g[1],b.g[2]];b.d=null;b.d=a.o?a.o(f,c,d,a.c)||null:null;null===b.d&&(e?a.n||(V(a,100156),a.n=true):b.d=c[0]);} function ta(a,b,c){var d=[null,null,null,null];d[0]=b.a.d;d[1]=c.a.d;sa(a,b.a,d,[.5,.5,0,0],false);E(b,c);}function ua(a,b,c,d,e){var f=Math.abs(b.b-a.b)+Math.abs(b.a-a.a),g=Math.abs(c.b-a.b)+Math.abs(c.a-a.a),h=e+1;d[e]=.5*g/(f+g);d[h]=.5*f/(f+g);a.g[0]+=d[e]*b.g[0]+d[h]*c.g[0];a.g[1]+=d[e]*b.g[1]+d[h]*c.g[1];a.g[2]+=d[e]*b.g[2]+d[h]*c.g[2];} function qa(a,b){var c=S(b),d=b.a,e=c.a;if(u(d.a,e.a)){if(0=l?W(c,l):u(h[g[l>>1]],h[g[l]])?W(c,l):va(c,l));h[f]=null;k[f]=c.b;c.b=f;}else for(c.c[-(f+1)]=null;0x(d.b.a,e.a,d.a))return false;R(b).b=b.b=true;K(d.b);E(J(e),d);}return true} function wa(a,b){var c=S(b),d=b.a,e=c.a,f=d.a,g=e.a,h=d.b.a,k=e.b.a,l=new N;if(f===g||Math.min(f.a,h.a)>Math.max(g.a,k.a))return false;if(u(f,g)){if(0x(h,g,f))return false;var r=h,p=f,q=k,y=g,m,w;u(r,p)||(m=r,r=p,p=m);u(q,y)||(m=q,q=y,y=m);u(r,q)||(m=r,r=q,q=m,m=p,p=y,y=m);u(q,p)?u(p,y)?(m=v(r,q,p),w=v(q,p,y),0>m+w&&(m=-m,w=-w),l.b=A(m,q.b,w,p.b)):(m=x(r,q,p),w=-x(r,y,p),0>m+w&&(m=-m,w=-w),l.b=A(m,q.b,w,y.b)):l.b=(q.b+p.b)/2;z(r,p)||(m=r,r=p,p=m);z(q,y)|| (m=q,q=y,y=m);z(r,q)||(m=r,r=q,q=m,m=p,p=y,y=m);z(q,p)?z(p,y)?(m=aa(r,q,p),w=aa(q,p,y),0>m+w&&(m=-m,w=-w),l.a=A(m,q.a,w,p.a)):(m=ba(r,q,p),w=-ba(r,y,p),0>m+w&&(m=-m,w=-w),l.a=A(m,q.a,w,y.a)):l.a=(q.a+p.a)/2;u(l,a.a)&&(l.b=a.a.b,l.a=a.a.a);r=u(f,g)?f:g;u(r,l)&&(l.b=r.b,l.a=r.a);if(t(l,f)||t(l,g))return qa(a,b),false;if(!t(h,a.a)&&0<=x(h,a.a,l)||!t(k,a.a)&&0>=x(k,a.a,l)){if(k===a.a)return K(d.b),E(e.b,d),b=ka(b),d=S(b).a,T(a,S(b),c),U(a,b,J(d),d,d,true),true;if(h===a.a){K(e.b);E(d.e,J(e));f=c=b;g=f.a.b.a; do f=R(f);while(f.a.b.a===g);b=f;f=S(b).a.b.c;c.a=J(e);e=T(a,c,null);U(a,b,e.c,d.b.c,f,true);return true}0<=x(h,a.a,l)&&(R(b).b=b.b=true,K(d.b),d.a.b=a.a.b,d.a.a=a.a.a);0>=x(k,a.a,l)&&(b.b=c.b=true,K(e.b),e.a.b=a.a.b,e.a.a=a.a.a);return false}K(d.b);K(e.b);E(J(e),d);d.a.b=l.b;d.a.a=l.a;d.a.h=xa(a.e,d.a);d=d.a;e=[0,0,0,0];l=[f.d,h.d,g.d,k.d];d.g[0]=d.g[1]=d.g[2]=0;ua(d,f,h,e,0);ua(d,g,k,e,2);sa(a,d,l,e,true);R(b).b=b.b=c.b=true;return false} function ra(a,b){for(var c=S(b);;){for(;c.b;)b=c,c=S(c);if(!b.b&&(c=b,b=R(b),null===b||!b.b))break;b.b=false;var d=b.a,e=c.a,f;if(f=d.b.a!==e.b.a)a:{f=b;var g=S(f),h=f.a,k=g.a,l=void 0;if(u(h.b.a,k.b.a)){if(0>x(h.b.a,k.b.a,h.a)){f=false;break a}R(f).b=f.b=true;l=K(h);E(k.b,l);l.d.c=f.d;}else {if(0e;++e){var f=a[e];-1e150>f&&(f=-1e150,c=true);1E150h;++h){var k=a.g[h];kb[h]&&(b[h]=k,c[h]=a);}a=0;b[1]-f[1]>b[0]-f[0]&&(a=1);b[2]-f[2]>b[a]-f[a]&&(a=2);if(f[a]>=b[a])e[0]=0,e[1]=0,e[2]=1;else {b=0;f=g[a];c=c[a];g=[0,0,0];f=[f.g[0]-c.g[0],f.g[1]-c.g[1],f.g[2]-c.g[2]];h=[0,0,0];for(a=d.e;a!==d;a= a.e)h[0]=a.g[0]-c.g[0],h[1]=a.g[1]-c.g[1],h[2]=a.g[2]-c.g[2],g[0]=f[1]*h[2]-f[2]*h[1],g[1]=f[2]*h[0]-f[0]*h[2],g[2]=f[0]*h[1]-f[1]*h[0],k=g[0]*g[0]+g[1]*g[1]+g[2]*g[2],k>b&&(b=k,e[0]=g[0],e[1]=g[1],e[2]=g[2]);0>=b&&(e[0]=e[1]=e[2]=0,e[ha(f)]=1);}d=true;}g=ha(e);a=this.b.c;b=(g+1)%3;c=(g+2)%3;g=0=b.f)){do e+=(b.a.b-b.b.a.b)*(b.a.a+b.b.a.a),b=b.e;while(b!==a.a)}if(0>e)for(e=this.b.c,d=e.e;d!== e;d=d.e)d.a=-d.a;}this.n=false;e=this.b.b;for(a=e.h;a!==e;a=d)if(d=a.h,b=a.e,t(a.a,a.b.a)&&a.e.e!==a&&(ta(this,b,a),I(a),a=b,b=a.e),b.e===a){if(b!==a){if(b===d||b===d.b)d=d.h;I(b);}if(a===d||a===d.b)d=d.h;I(a);}this.e=e=new Da;d=this.b.c;for(a=d.e;a!==d;a=a.e)a.h=xa(e,a);Ea(e);this.f=new Aa(this);za(this,-O);for(za(this,O);null!==(e=Fa(this.e));){for(;;){a:if(a=this.e,0===a.a)d=Ga(a.b);else if(d=a.c[a.d[a.a-1]],0!==a.b.a&&(a=Ga(a.b),u(a,d))){d=a;break a}if(null===d||!t(d,e))break;d=Fa(this.e);ta(this,e.c, d.c);}ya(this,e);}this.a=this.f.a.a.b.a.a;for(e=0;null!==(d=this.f.a.a.b);)d.h||++e,Q(d);this.f=null;e=this.e;e.b=null;e.d=null;this.e=e.c=null;e=this.b;for(a=e.a.b;a!==e.a;a=d)d=a.b,a=a.a,a.e.e===a&&(P(a.c,a),I(a));if(!this.n){e=this.b;if(this.m)for(a=e.b.h;a!==e.b;a=d)d=a.h,a.b.d.c!==a.d.c?a.f=a.d.c?1:-1:I(a);else for(a=e.a.b;a!==e.a;a=d)if(d=a.b,a.c){for(a=a.a;u(a.b.a,a.a);a=a.c.b);for(;u(a.a,a.b.a);a=a.e);b=a.c.b;for(c=void 0;a.e!==b;)if(u(a.b.a,b.a)){for(;b.e!==a&&(ca(b.e)||0>=x(b.a,b.b.a,b.e.b.a));)c= L(b.e,b),b=c.b;b=b.c.b;}else {for(;b.e!==a&&(da(a.c.b)||0<=x(a.b.a,a.a,a.c.b.a));)c=L(a,a.c.b),a=c.b;a=a.e;}for(;b.e.e!==a;)c=L(b.e,b),b=c.b;}if(this.h||this.i||this.k||this.l)if(this.m)for(e=this.b,d=e.a.b;d!==e.a;d=d.b){if(d.c){this.h&&this.h(2,this.c);a=d.a;do this.k&&this.k(a.a.d,this.c),a=a.e;while(a!==d.a);this.i&&this.i(this.c);}}else {e=this.b;d=!!this.l;a=false;b=-1;for(c=e.a.d;c!==e.a;c=c.d)if(c.c){a||(this.h&&this.h(4,this.c),a=true);g=c.a;do d&&(f=g.b.d.c?0:1,b!==f&&(b=f,this.l&&this.l(!!b,this.c))), this.k&&this.k(g.a.d,this.c),g=g.e;while(g!==c.a)}a&&this.i&&this.i(this.c);}if(this.r){e=this.b;for(a=e.a.b;a!==e.a;a=d)if(d=a.b,!a.c){b=a.a;c=b.e;g=void 0;do g=c,c=g.e,g.d=null,null===g.b.d&&(g.c===g?F(g.a,null):(g.a.c=g.c,H(g,J(g))),f=g.b,f.c===f?F(f.a,null):(f.a.c=f.c,H(f,J(f))),fa(g));while(g!==b);b=a.d;a=a.b;a.d=b;b.b=a;}this.r(this.b);this.c=this.b=null;return}}this.b=this.c=null;}; function Z(a,b){if(a.d!==b)for(;a.d!==b;)if(a.dc.f&&(c.f*=2,c.c=Ja(c.c,c.f+1));var e;0===c.b?e=d:(e=c.b,c.b=c.c[c.b]);c.e[e]=b;c.c[e]=d;c.d[d]=e;c.h&&va(c,d);return e}c=a.a++;a.c[c]=b;return -(c+1)} function Fa(a){if(0===a.a)return Ka(a.b);var b=a.c[a.d[a.a-1]];if(0!==a.b.a&&u(Ga(a.b),b))return Ka(a.b);do--a.a;while(0a.a||u(d[g],d[k])){c[f]=g;e[g]=f;break}c[f]=k;e[k]=f;f=h;}}function va(a,b){for(var c=a.d,d=a.e,e=a.c,f=b,g=c[f];;){var h=f>>1,k=c[h];if(0===h||u(d[k],d[g])){c[f]=g;e[g]=f;break}c[f]=k;e[k]=f;f=h;}}function ma(){this.e=this.a=null;this.f=0;this.c=this.b=this.h=this.d=false;}function S(a){return a.e.c.b}function R(a){return a.e.a.b}commonjsGlobal.libtess={GluTesselator:X,windingRule:{GLU_TESS_WINDING_ODD:100130,GLU_TESS_WINDING_NONZERO:100131,GLU_TESS_WINDING_POSITIVE:100132,GLU_TESS_WINDING_NEGATIVE:100133,GLU_TESS_WINDING_ABS_GEQ_TWO:100134},primitiveType:{GL_LINE_LOOP:2,GL_TRIANGLES:4,GL_TRIANGLE_STRIP:5,GL_TRIANGLE_FAN:6},errorType:{GLU_TESS_MISSING_BEGIN_POLYGON:100151,GLU_TESS_MISSING_END_POLYGON:100153,GLU_TESS_MISSING_BEGIN_CONTOUR:100152,GLU_TESS_MISSING_END_CONTOUR:100154,GLU_TESS_COORD_TOO_LARGE:100155,GLU_TESS_NEED_COMBINE_CALLBACK:100156}, gluEnum:{GLU_TESS_MESH:100112,GLU_TESS_TOLERANCE:100142,GLU_TESS_WINDING_RULE:100140,GLU_TESS_BOUNDARY_ONLY:100141,GLU_INVALID_ENUM:100900,GLU_INVALID_VALUE:100901,GLU_TESS_BEGIN:100100,GLU_TESS_VERTEX:100101,GLU_TESS_END:100102,GLU_TESS_ERROR:100103,GLU_TESS_EDGE_FLAG:100104,GLU_TESS_COMBINE:100105,GLU_TESS_BEGIN_DATA:100106,GLU_TESS_VERTEX_DATA:100107,GLU_TESS_END_DATA:100108,GLU_TESS_ERROR_DATA:100109,GLU_TESS_EDGE_FLAG_DATA:100110,GLU_TESS_COMBINE_DATA:100111}};X.prototype.gluDeleteTess=X.prototype.x; X.prototype.gluTessProperty=X.prototype.B;X.prototype.gluGetTessProperty=X.prototype.y;X.prototype.gluTessNormal=X.prototype.A;X.prototype.gluTessCallback=X.prototype.z;X.prototype.gluTessVertex=X.prototype.C;X.prototype.gluTessBeginPolygon=X.prototype.u;X.prototype.gluTessBeginContour=X.prototype.t;X.prototype.gluTessEndContour=X.prototype.v;X.prototype.gluTessEndPolygon=X.prototype.w; { module.exports = commonjsGlobal.libtess; } } (libtess_min)); var libtess_minExports = libtess_min.exports; var libtess = /*@__PURE__*/getDefaultExportFromCjs(libtess_minExports); class RenderBuffer { constructor(size, src, dst, attr, renderer, map) { this.size = size; // the number of FLOATs in each vertex this.src = src; // the name of the model's source array this.dst = dst; // the name of the geometry's buffer this.attr = attr; // the name of the vertex attribute this._renderer = renderer; this.map = map; // optional, a transformation function to apply to src } default(cb) { this.default = cb; return this; } /** * Enables and binds the buffers used by shader when the appropriate data exists in geometry. * Must always be done prior to drawing geometry in WebGL. * @param {p5.Geometry} geometry Geometry that is going to be drawn * @param {p5.Shader} shader Active shader * @private */ _prepareBuffer(geometry, shader) { this._renderer._prepareBuffer(this, geometry, shader); } } function renderBuffer(p5, fn) { p5.RenderBuffer = RenderBuffer; } if (typeof p5 !== 'undefined') { renderBuffer(p5); } const INITIAL_BUFFER_STRIDES = { vertices: 1, vertexNormals: 1, vertexColors: 4, vertexStrokeColors: 4, uvs: 2 }; // The total number of properties per vertex, before additional // user attributes are added. const INITIAL_VERTEX_SIZE = Object.values(INITIAL_BUFFER_STRIDES).reduce((acc, next) => acc + next); class ShapeBuilder { constructor(renderer) { this.renderer = renderer; this.shapeMode = PATH; this.geometry = new Geometry( undefined, undefined, undefined, this.renderer ); this.geometry.gid = '__IMMEDIATE_MODE_GEOMETRY__'; this.contourIndices = []; this._useUserVertexProperties = undefined; this._bezierVertex = []; this._quadraticVertex = []; this._curveVertex = []; // Used to distinguish between user calls to vertex() and internal calls this.isProcessingVertices = false; // Used for converting shape outlines into triangles for rendering this._tessy = this._initTessy(); this.tessyVertexSize = INITIAL_VERTEX_SIZE; this.bufferStrides = { ...INITIAL_BUFFER_STRIDES }; } constructFromContours(shape, contours) { if (this._useUserVertexProperties){ this._resetUserVertexProperties(); } this.geometry.reset(); this.contourIndices = []; // TODO: handle just some contours having non-PATH mode this.shapeMode = shape.contours[0].kind; const shouldProcessEdges = !!this.renderer.states.strokeColor; const userVertexPropertyHelpers = {}; if (shape.userVertexProperties) { this._useUserVertexProperties = true; for (const key in shape.userVertexProperties) { const name = shape.vertexPropertyName(key); const prop = this.geometry._userVertexPropertyHelper( name, [], shape.userVertexProperties[key] ); userVertexPropertyHelpers[key] = prop; this.tessyVertexSize += prop.getDataSize(); this.bufferStrides[prop.getSrcName()] = prop.getDataSize(); this.renderer.buffers.user.push( new RenderBuffer( prop.getDataSize(), prop.getSrcName(), prop.getDstName(), name, this.renderer ) ); } } else { this._useUserVertexProperties = false; } for (const contour of contours) { this.contourIndices.push(this.geometry.vertices.length); for (const vertex of contour) { // WebGL 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.shapeMode === QUADS) { // A finished quad turned into triangles should leave 6 vertices in the // buffer: // 0--3 0 3--5 // | | --> | \ \ | // 1--2 1--2 4 // When vertex index 3 is being added, add the necessary duplicates. if (this.geometry.vertices.length % 6 === 3) { for (const key in this.bufferStrides) { const stride = this.bufferStrides[key]; const buffer = this.geometry[key]; buffer.push( ...buffer.slice( buffer.length - 3 * stride, buffer.length - 2 * stride ), ...buffer.slice(buffer.length - stride, buffer.length) ); } } } this.geometry.vertices.push(vertex.position); this.geometry.vertexNormals.push(vertex.normal || new Vector(0, 0, 0)); this.geometry.uvs.push( vertex.textureCoordinates.x, vertex.textureCoordinates.y ); if (this.renderer.states.fillColor) { this.geometry.vertexColors.push(...vertex.fill.array()); } else { this.geometry.vertexColors.push(0, 0, 0, 0); } if (this.renderer.states.strokeColor) { this.geometry.vertexStrokeColors.push(...vertex.stroke.array()); } else { this.geometry.vertexStrokeColors.push(0, 0, 0, 0); } for (const key in userVertexPropertyHelpers) { const prop = userVertexPropertyHelpers[key]; if (key in vertex) { prop.setCurrentData(vertex[key]); } prop.pushCurrentData(); } } } if (shouldProcessEdges) { this.geometry.edges = this._calculateEdges( this.shapeMode, this.geometry.vertices ); } if (shouldProcessEdges && !this.renderer.geometryBuilder) { this.geometry._edgesToVertices(); } if (this.shapeMode === PATH) { this.isProcessingVertices = true; this._tesselateShape(); this.isProcessingVertices = false; } else if (this.shapeMode === QUAD_STRIP) { // The only difference between these two modes is which edges are // displayed, so after we've updated the edges, we switch the mode // to one that native WebGL knows how to render. this.shapeMode = TRIANGLE_STRIP; } else if (this.shapeMode === QUADS) { // We translate QUADS to TRIANGLES when vertices are being added, // since QUADS is just a p5 mode, whereas TRIANGLES is also a mode // that native WebGL knows how to render. Once we've processed edges, // everything should be set up for TRIANGLES mode. this.shapeMode = TRIANGLES; } if ( this.renderer.states.textureMode === IMAGE && this.renderer.states._tex !== null && this.renderer.states._tex.width > 0 && this.renderer.states._tex.height > 0 ) { this.geometry.uvs = this.geometry.uvs.map((val, i) => { if (i % 2 === 0) { return val / this.renderer.states._tex.width; } else { return val / this.renderer.states._tex.height; } }); } } _resetUserVertexProperties() { const properties = this.geometry.userVertexProperties; for (const propName in properties){ const prop = properties[propName]; delete this.bufferStrides[propName]; prop.delete(); } this._useUserVertexProperties = false; this.tessyVertexSize = INITIAL_VERTEX_SIZE; this.geometry.userVertexProperties = {}; } /** * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and * tesselates shapes when applicable. * @private * @returns {Number[]} indices for custom shape vertices indicating edges. */ _calculateEdges( shapeMode, verts ) { const res = []; let i = 0; const contourIndices = this.contourIndices.slice(); let contourStart = -1; switch (shapeMode) { case TRIANGLE_STRIP: for (i = 0; i < verts.length - 2; i++) { res.push([i, i + 1]); res.push([i, i + 2]); } res.push([i, i + 1]); break; case TRIANGLE_FAN: for (i = 1; i < verts.length - 1; i++) { res.push([0, i]); res.push([i, i + 1]); } res.push([0, verts.length - 1]); break; case TRIANGLES: for (i = 0; i < verts.length - 2; i = i + 3) { res.push([i, i + 1]); res.push([i + 1, i + 2]); res.push([i + 2, i]); } break; case LINES: for (i = 0; i < verts.length - 1; i = i + 2) { res.push([i, i + 1]); } break; case QUADS: // Quads have been broken up into two triangles by `vertex()`: // 0 3--5 // | \ \ | // 1--2 4 for (i = 0; i < verts.length - 5; i += 6) { res.push([i, i + 1]); res.push([i + 1, i + 2]); res.push([i + 2, i + 5]); res.push([i + 5, i]); } break; case 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 + 1, i + 3]); res.push([i, i + 2]); } res.push([i, i + 1]); break; default: // TODO: handle contours in other modes too for (i = 0; i < verts.length; i++) { if (i === contourIndices[0]) { contourStart = contourIndices.shift(); } else if ( verts[contourStart] && verts[i].equals(verts[contourStart]) ) { res.push([i - 1, contourStart]); } else { res.push([i - 1, i]); } } break; } return res; } /** * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. * @private */ _tesselateShape() { // const contours = [[]]; const contours = []; for (let i = 0; i < this.geometry.vertices.length; i++) { if ( this.contourIndices.length > 0 && this.contourIndices[0] === i ) { this.contourIndices.shift(); contours.push([]); } contours[contours.length-1].push( this.geometry.vertices[i].x, this.geometry.vertices[i].y, this.geometry.vertices[i].z, this.geometry.uvs[i * 2], this.geometry.uvs[i * 2 + 1], this.geometry.vertexColors[i * 4], this.geometry.vertexColors[i * 4 + 1], this.geometry.vertexColors[i * 4 + 2], this.geometry.vertexColors[i * 4 + 3], this.geometry.vertexNormals[i].x, this.geometry.vertexNormals[i].y, this.geometry.vertexNormals[i].z ); for (const propName in this.geometry.userVertexProperties) { const prop = this.geometry.userVertexProperties[propName]; const start = i * prop.getDataSize(); const end = start + prop.getDataSize(); const vals = prop.getSrcArray().slice(start, end); contours[contours.length-1].push(...vals); } } // Normalize nearly identical consecutive vertices to prevent tessellation artifacts // This addresses numerical precision issues in libtess when consecutive vertices // have coordinates that are almost (but not exactly) equal (e.g., differing by ~1e-8) const epsilon = 1e-6; for (const contour of contours) { const stride = this.tessyVertexSize; for (let i = stride; i < contour.length; i += stride) { const prevX = contour[i - stride]; const prevY = contour[i - stride + 1]; const currX = contour[i]; const currY = contour[i + 1]; if (Math.abs(currX - prevX) < epsilon) { contour[i] = prevX; } if (Math.abs(currY - prevY) < epsilon) { contour[i + 1] = prevY; } } } const polyTriangles = this._triangulate(contours); // If there were no valid faces, we still want to use the original vertices // for strokes, so we'll stop here. if (polyTriangles.length === 0) { return; } // TODO: handle non-PATH shape modes that have contours this.shapeMode = TRIANGLES; const originalVertices = this.geometry.vertices; this.geometry.vertices = []; this.geometry.vertexNormals = []; this.geometry.uvs = []; for (const propName in this.geometry.userVertexProperties){ const prop = this.geometry.userVertexProperties[propName]; prop.resetSrcArray(); } const colors = []; for ( let j = 0, polyTriLength = polyTriangles.length; j < polyTriLength; j = j + this.tessyVertexSize ) { colors.push(...polyTriangles.slice(j + 5, j + 9)); this.geometry.vertexNormals.push( new Vector(...polyTriangles.slice(j + 9, j + 12)) ); { let offset = 12; for (const propName in this.geometry.userVertexProperties){ const prop = this.geometry.userVertexProperties[propName]; const size = prop.getDataSize(); const start = j + offset; const end = start + size; prop.setCurrentData(polyTriangles.slice(start, end)); prop.pushCurrentData(); offset += size; } } this.geometry.vertices.push(new Vector(...polyTriangles.slice(j, j + 3))); this.geometry.uvs.push(...polyTriangles.slice(j + 3, j + 5)); } if (this.renderer.geometryBuilder) { // Tesselating the face causes the indices of edge vertices to stop being // correct. When rendering, this is not a problem, since _edgesToVertices // will have been called before this, and edge vertex indices are no longer // needed. However, the geometry builder still needs this information, so // when one is active, we need to update the indices. // // We record index mappings in a Map so that once we have found a // corresponding vertex, we don't need to loop to find it again. const newIndex = new Map(); this.geometry.edges = this.geometry.edges.map(edge => edge.map(origIdx => { if (!newIndex.has(origIdx)) { const orig = originalVertices[origIdx]; let newVertIndex = this.geometry.vertices.findIndex( v => orig.x === v.x && orig.y === v.y && orig.z === v.z ); if (newVertIndex === -1) { // The tesselation process didn't output a vertex with the exact // coordinate as before, potentially due to numerical issues. This // doesn't happen often, but in this case, pick the closest point let closestDist = Infinity; let closestIndex = 0; for ( let i = 0; i < this.geometry.vertices.length; i++ ) { const vert = this.geometry.vertices[i]; const dX = orig.x - vert.x; const dY = orig.y - vert.y; const dZ = orig.z - vert.z; const dist = dX*dX + dY*dY + dZ*dZ; if (dist < closestDist) { closestDist = dist; closestIndex = i; } } newVertIndex = closestIndex; } newIndex.set(origIdx, newVertIndex); } return newIndex.get(origIdx); })); } this.geometry.vertexColors = colors; } _initTessy() { // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { for (const element of data) { polyVertArray.push(element); } } function begincallback(type) { if (type !== libtess.primitiveType.GL_TRIANGLES) { console.log(`expected TRIANGLES but got type: ${type}`); } } function errorcallback(errno) { console.log('error callback'); console.log(`error number: ${errno}`); } // callback for when segments intersect and must be split const combinecallback = (coords, data, weight) => { const result = new Array(this.tessyVertexSize).fill(0); for (let i = 0; i < weight.length; i++) { for (let j = 0; j < result.length; j++) { if (weight[i] === 0 || !data[i]) continue; result[j] += data[i][j] * weight[i]; } } return result; }; function edgeCallback(flag) { // don't really care about the flag, but need no-strip/no-fan behavior } const tessy = new libtess.GluTesselator(); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); tessy.gluTessProperty( libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_NONZERO ); return tessy; } /** * Runs vertices through libtess to convert them into triangles * @private */ _triangulate(contours) { // libtess will take 3d verts and flatten to a plane for tesselation. // libtess is capable of calculating a plane to tesselate on, but // if all of the vertices have the same z values, we'll just // assume the face is facing the camera, letting us skip any performance // issues or bugs in libtess's automatic calculation. const z = contours[0] ? contours[0][2] : undefined; let allSameZ = true; for (const contour of contours) { for ( let j = 0; j < contour.length; j += this.tessyVertexSize ) { if (contour[j + 2] !== z) { allSameZ = false; break; } } } if (allSameZ) { this._tessy.gluTessNormal(0, 0, 1); } else { // Let libtess pick a plane for us this._tessy.gluTessNormal(0, 0, 0); } const triangleVerts = []; this._tessy.gluTessBeginPolygon(triangleVerts); for (const contour of contours) { this._tessy.gluTessBeginContour(); for ( let j = 0; j < contour.length; j += this.tessyVertexSize ) { const coords = contour.slice( j, j + this.tessyVertexSize ); this._tessy.gluTessVertex(coords, coords); } this._tessy.gluTessEndContour(); } // finish polygon this._tessy.gluTessEndPolygon(); return triangleVerts; } } class GeometryBufferCache { constructor(renderer) { this.renderer = renderer; this.cache = {}; } numCached() { return Object.keys(this.cache).length; } isCached(gid) { return this.cache[gid] !== undefined; } getGeometryByID(gid) { return this.cache[gid]?.geometry; } getCached(model) { return this.getCachedID(model.gid); } getCachedID(gid) { return this.cache[gid]; } ensureCached(geometry) { const gid = geometry.gid; if (!gid) { throw new Error('The p5.Geometry you passed in has no gid property!'); } if (this.isCached(gid)) return this.getCached(geometry); // Cache maintenance this.freeBuffers(gid); if (Object.keys(this.cache).length > 1000) { const key = Object.keys(this.cache)[0]; this.freeBuffers(key); } const buffers = { geometry }; this.cache[gid] = buffers; const indices = geometry.faces.length ? geometry.faces.flat() : null; // Determine index buffer type let indexType = null; if (indices) { // If any face references a vertex with an index greater than the maximum // un-singed 16 bit integer, then we need to use a Uint32Array instead of a // Uint16Array const hasVertexIndicesOverMaxUInt16 = indices.some(i => i > 65535); indexType = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; } this.renderer._ensureGeometryBuffers(buffers, indices, indexType); return buffers; } freeBuffers(gid) { const buffers = this.cache[gid]; if (!buffers) { return; } delete this.cache[gid]; this.renderer._freeBuffers(buffers); } } const filterParamDefaults = { [BLUR]: 3, [POSTERIZE]: 4, [THRESHOLD]: 0.5 }; /** * @module Typography * @requires core */ const DefaultFill = '#000000'; const textCoreConstants = { IDEOGRAPHIC: 'ideographic', _CTX_MIDDLE: 'middle', _TEXT_BOUNDS: '_textBoundsSingle', _FONT_BOUNDS: '_fontBoundsSingle', HANGING: 'hanging', START: 'start', END: 'end' }; function textCore(p5, fn) { const LeadingScale = 1.275; const LinebreakRe = /\r?\n/g; const CommaDelimRe = /,\s+/; const QuotedRe = /^".*"$/; const SpecialCharRe = /[^\x00-\x7F]/; // Non-ascii const TabsRe = /\t/g; const FontVariationSettings = 'fontVariationSettings'; const VariableAxes = ['wght', 'wdth', 'ital', 'slnt', 'opsz']; const VariableAxesRe = new RegExp(`(?:${VariableAxes.join('|')})`); const textFunctions = [ 'text', 'textAlign', 'textAscent', 'textDescent', 'textLeading', 'textMode', 'textFont', 'textSize', 'textStyle', 'textWidth', 'textWrap', 'textBounds', 'textDirection', 'textProperty', 'textProperties', 'fontBounds', 'fontWidth', 'fontAscent', 'fontDescent', 'textWeight' ]; /** * Draws text to the canvas. * * The first parameter, `str`, is the text to be drawn. The second and third * parameters, `x` and `y`, set the coordinates of the text's bottom-left * corner. See textAlign() for other ways to * align text. * * The fourth and fifth parameters, `maxWidth` and `maxHeight`, are optional. * They set the dimensions of the invisible rectangle containing the text. By * default, they set its maximum width and height. See * rectMode() for other ways to define the * rectangular text box. Text will wrap to fit within the text box. Text * outside of the box won't be drawn. * * Text can be styled a few ways. Call the fill() * function to set the text's fill color. Call * stroke() and * strokeWeight() to set the text's outline. * Call textSize() and * textFont() to set the text's size and font, * respectively. * * Note: `WEBGL` mode only supports fonts loaded with * loadFont(). Calling * stroke() has no effect in `WEBGL` mode. * * @method text * @param {String|Object|Array|Number|Boolean} str text to be displayed. * @param {Number} x x-coordinate of the text box. * @param {Number} y y-coordinate of the text box. * @param {Number} [maxWidth] maximum width of the text box. See * rectMode() for * other options. * @param {Number} [maxHeight] maximum height of the text box. See * rectMode() for * other options. * * @for p5 * @example * function setup() { * createCanvas(100, 100); * background(200); * text('hi', 50, 50); * * describe('The text "hi" written in black in the middle of a gray square.'); * } * * @example * function setup() { * createCanvas(100, 100); * background('skyblue'); * textSize(100); * text('🌈', 0, 100); * * describe('A rainbow in a blue sky.'); * } * * @example * function setup() { * createCanvas(100, 100); * textSize(32); * fill(255); * stroke(0); * strokeWeight(4); * text('hi', 50, 50); * * describe('The text "hi" written in white with a black outline.'); * } * * @example * function setup() { * createCanvas(100, 100); * background('black'); * textSize(22); * fill('yellow'); * text('rainbows', 6, 20); * fill('cornflowerblue'); * text('rainbows', 6, 45); * fill('tomato'); * text('rainbows', 6, 70); * fill('limegreen'); * text('rainbows', 6, 95); * * describe('The text "rainbows" written on several lines, each in a different color.'); * } * * @example * function setup() { * createCanvas(100, 100); * background(200); * let s = 'The quick brown fox jumps over the lazy dog.'; * text(s, 10, 10, 70, 80); * * describe('The sample text "The quick brown fox..." written in black across several lines.'); * } * * @example * function setup() { * createCanvas(100, 100); * background(200); * rectMode(CENTER); * let s = 'The quick brown fox jumps over the lazy dog.'; * text(s, 50, 50, 70, 80); * * describe('The sample text "The quick brown fox..." written in black across several lines.'); * } * * @example * let font; * * async function setup() { * createCanvas(100, 100, WEBGL); * font = await loadFont('assets/inconsolata.otf'); * textFont(font); * textSize(32); * textAlign(CENTER, CENTER); * } * * function draw() { * background(200); * rotateY(frameCount / 30); * text('p5*js', 0, 0); * * describe('The text "p5*js" written in white and spinning in 3D.'); * } */ /** * Sets the way text is aligned when text() is called. * * By default, calling `text('hi', 10, 20)` places the bottom-left corner of * the text's bounding box at (10, 20). * * The first parameter, `horizAlign`, changes the way * text() interprets x-coordinates. By default, the * x-coordinate sets the left edge of the bounding box. `textAlign()` accepts * the following values for `horizAlign`: `LEFT`, `CENTER`, or `RIGHT`. * * The second parameter, `vertAlign`, is optional. It changes the way * text() interprets y-coordinates. By default, the * y-coordinate sets the bottom edge of the bounding box. `textAlign()` * accepts the following values for `vertAlign`: `TOP`, `BOTTOM`, `CENTER`, * or `BASELINE`. * * Calling `textAlign()` without arguments returns the current alignment settings. * * @method textAlign * @for p5 * @param {LEFT|CENTER|RIGHT} [horizAlign] horizontal alignment * @param {TOP|BOTTOM|CENTER|BASELINE} [vertAlign] vertical alignment * @returns {Object} If no arguments are provided, returns an object with current horizontal and vertical alignment * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw a vertical line. * strokeWeight(0.5); * line(50, 0, 50, 100); * * // Top line. * textSize(16); * textAlign(RIGHT); * text('ABCD', 50, 30); * * // Middle line. * textAlign(CENTER); * text('EFGH', 50, 50); * * // Bottom line. * textAlign(LEFT); * text('IJKL', 50, 70); * * describe('The letters ABCD displayed at top-left, EFGH at center, and IJKL at bottom-right. A vertical line divides the canvas in half.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * strokeWeight(0.5); * * // First line. * line(0, 12, width, 12); * textAlign(CENTER, TOP); * text('TOP', 50, 12); * * // Second line. * line(0, 37, width, 37); * textAlign(CENTER, CENTER); * text('CENTER', 50, 37); * * // Third line. * line(0, 62, width, 62); * textAlign(CENTER, BASELINE); * text('BASELINE', 50, 62); * * // Fourth line. * line(0, 97, width, 97); * textAlign(CENTER, BOTTOM); * text('BOTTOM', 50, 97); * * describe('The words "TOP", "CENTER", "BASELINE", and "BOTTOM" each drawn relative to a horizontal line. Their positions demonstrate different vertical alignments.'); * } */ /** * Returns the ascent of the text. * * The `textAscent()` function calculates the distance from the baseline to the * highest point of the current font. This value represents the ascent, which is essential * for determining the overall height of the text along with `textDescent()`. If * a text string is provided as an argument, the ascent is calculated based on that specific * string; otherwise, the ascent of the current font is returned. * * @method textAscent * @for p5 * * @param {String} [txt] - (Optional) The text string for which to calculate the ascent. * If omitted, the function returns the ascent for the current font. * @returns {Number} The ascent value in pixels. * * @example * function setup() { * createCanvas(400, 300); * background(220); * * textSize(48); * textAlign(LEFT, BASELINE); * textFont('Georgia'); * * let s = "Hello, p5.js!"; * let x = 50, y = 150; * * fill(0); * text(s, x, y); * * // Get the ascent of the current font * let asc = textAscent(); * * // Draw a red line at the baseline and a blue line at the ascent position * stroke('red'); * line(x, y, x + 200, y); // Baseline * stroke('blue'); * line(x, y - asc, x + 200, y - asc); // Ascent (top of text) * * noStroke(); * fill(0); * textSize(16); * text("textAscent: " + asc.toFixed(2) + " pixels", x, y - asc - 10); * } * * @example * let font; * * async function setup() { * font = await loadFont('assets/inconsolata.otf'); * * createCanvas(100, 100); * * background(200); * * // Style the text. * textFont(font); * * // Different for each font. * let fontScale = 0.8; * * let baseY = 75; * strokeWeight(0.5); * * // Draw small text. * textSize(24); * text('dp', 0, baseY); * * // Draw baseline and ascent. * let a = textAscent() * fontScale; * line(0, baseY, 23, baseY); * line(23, baseY - a, 23, baseY); * * // Draw large text. * textSize(48); * text('dp', 45, baseY); * * // Draw baseline and ascent. * a = textAscent() * fontScale; * line(45, baseY, 91, baseY); * line(91, baseY - a, 91, baseY); * * describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends upward from each baseline to the top of the "d".'); * } */ /** * Returns the descent of the text. * * The `textDescent()` function calculates the distance from the baseline to the * lowest point of the current font. This value represents the descent, which, when combined * with the ascent (from `textAscent()`), determines the overall vertical span of the text. * If a text string is provided as an argument, the descent is calculated based on that specific string; * otherwise, the descent of the current font is returned. * * @method textDescent * @for p5 * * @param {String} [txt] - (Optional) The text string for which to calculate the descent. * If omitted, the function returns the descent for the current font. * @returns {Number} The descent value in pixels. * * @example * function setup() { * createCanvas(400, 300); * background(220); * * textSize(48); * textAlign(LEFT, BASELINE); * textFont('Georgia'); * * let s = "Hello, p5.js!"; * let x = 50, y = 150; * * fill(0); * text(s, x, y); * * // Get the descent of the current font * let desc = textDescent(); * * // Draw a red line at the baseline and a blue line at the bottom of the text * stroke('red'); * line(x, y, x + 200, y); // Baseline * stroke('blue'); * line(x, y + desc, x + 200, y + desc); // Descent (bottom of text) * * noStroke(); * fill(0); * textSize(16); * text("textDescent: " + desc.toFixed(2) + " pixels", x, y + desc + 20); * } * * @example * let font; * * async function setup() { * font = await loadFont('assets/inconsolata.otf'); * * createCanvas(100, 100); * * background(200); * * // Style the font. * textFont(font); * * // Different for each font. * let fontScale = 0.9; * * let baseY = 75; * strokeWeight(0.5); * * // Draw small text. * textSize(24); * text('dp', 0, baseY); * * // Draw baseline and descent. * let d = textDescent() * fontScale; * line(0, baseY, 23, baseY); * line(23, baseY, 23, baseY + d); * * // Draw large text. * textSize(48); * text('dp', 45, baseY); * * // Draw baseline and descent. * d = textDescent() * fontScale; * line(45, baseY, 91, baseY); * line(91, baseY, 91, baseY + d); * * describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends downward from each baseline to the bottom of the "p".'); * } */ /** * Sets the spacing between lines of text when * text() is called. * * Note: Spacing is measured in pixels. * * Calling `textLeading()` without an argument returns the current spacing. * * @method textLeading * @for p5 * @param {Number} [leading] The new text leading to apply, in pixels * @returns {Number} If no arguments are provided, the current text leading * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // "\n" starts a new line of text. * let lines = 'one\ntwo'; * * // Left. * text(lines, 10, 25); * * // Right. * textLeading(30); * text(lines, 70, 25); * * describe('The words "one" and "two" written on separate lines twice. The words on the left have less vertical spacing than the words on the right.'); * } */ /** * Sets the font used by the text() function. * * The first parameter, `font`, sets the font. `textFont()` recognizes either * a p5.Font object or a string with the name of a * system font. For example, `'Courier New'`. * * The second parameter, `size`, is optional. It sets the font size in pixels. * This has the same effect as calling textSize(). * * Calling `textFont()` without arguments returns the current font. * * Note: `WEBGL` mode only supports fonts loaded with * loadFont(). * * @method textFont * @param {p5.Font|String|Object} [font] The font to apply * @param {Number} [size] An optional text size to apply. * @returns {String|p5.Font} If no arguments are provided, returns the current font * @for p5 * * @example * function setup() { * createCanvas(100, 100); * background(200); * textFont('Courier New'); * textSize(24); * text('hi', 35, 55); * * describe('The text "hi" written in a black, monospace font on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * background('black'); * fill('palegreen'); * textFont('Courier New', 10); * text('You turn to the left and see a door. Do you enter?', 5, 5, 90, 90); * text('>', 5, 70); * * describe('A text prompt from a game is written in a green, monospace font on a black background.'); * } * * @example * function setup() { * createCanvas(100, 100); * background(200); * textFont('Verdana'); * let currentFont = textFont(); * text(currentFont, 25, 50); * * describe('The text "Verdana" written in a black, sans-serif font on a gray background.'); * } * * @example * let fontRegular; * let fontItalic; * let fontBold; * * async function setup() { * createCanvas(100, 100); * fontRegular = await loadFont('assets/Regular.otf'); * fontItalic = await loadFont('assets/Italic.ttf'); * fontBold = await loadFont('assets/Bold.ttf'); * * background(200); * textFont(fontRegular); * text('I am Normal', 10, 30); * textFont(fontItalic); * text('I am Italic', 10, 50); * textFont(fontBold); * text('I am Bold', 10, 70); * * describe('The statements "I am Normal", "I am Italic", and "I am Bold" written in black on separate lines. The statements have normal, italic, and bold fonts, respectively.'); * } */ /** * Sets or gets the current text size. * * The `textSize()` function is used to specify the size of the text * that will be rendered on the canvas. When called with an argument, it sets the * text size to the specified value (which can be a number representing pixels or a * CSS-style string, e.g., '32px', '2em'). When called without an argument, it * returns the current text size in pixels. * * @method textSize * @for p5 * * @param {Number} size - The size to set for the text. * @returns {Number} If no arguments are provided, the current text size in pixels. * * @example * function setup() { * createCanvas(600, 200); * background(240); * * // Set the text size to 48 pixels * textSize(48); * textAlign(CENTER, CENTER); * textFont("Georgia"); * * // Draw text using the current text size * fill(0); * text("Hello, p5.js!", width / 2, height / 2); * * // Retrieve and display the current text size * let currentSize = textSize(); * fill(50); * textSize(16); * text("Current text size: " + currentSize, width / 2, height - 20); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Top. * textSize(12); * text('Font Size 12', 10, 30); * * // Middle. * textSize(14); * text('Font Size 14', 10, 60); * * // Bottom. * textSize(16); * text('Font Size 16', 10, 90); * * describe('The text "Font Size 12" drawn small, "Font Size 14" drawn medium, and "Font Size 16" drawn large.'); * } */ /** * @method textSize * @for p5 * @returns {Number} The current text size in pixels. */ /** * Sets the style for system fonts when * text() is called. * * The parameter, `style`, can be either `NORMAL`, `ITALIC`, `BOLD`, or * `BOLDITALIC`. * * `textStyle()` may be overridden by CSS styling. This function doesn't * affect fonts loaded with loadFont(). * * @method textStyle * @for p5 * @param {NORMAL|ITALIC|BOLD|BOLDITALIC} style The style to use * @returns {NORMAL|ITALIC|BOLD|BOLDITALIC} If no arguments are provided, the current style * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textSize(12); * textAlign(CENTER); * * // First row. * textStyle(NORMAL); * text('Normal', 50, 15); * * // Second row. * textStyle(ITALIC); * text('Italic', 50, 40); * * // Third row. * textStyle(BOLD); * text('Bold', 50, 65); * * // Fourth row. * textStyle(BOLDITALIC); * text('Bold Italic', 50, 90); * * describe('The words "Normal" displayed normally, "Italic" in italic, "Bold" in bold, and "Bold Italic" in bold italics.'); * } */ /** * @method textStyle * @for p5 * @returns {NORMAL|BOLD|ITALIC|BOLDITALIC} */ /** * Calculates the width of the given text string in pixels. * * The `textWidth()` function processes the provided text string to determine its tight bounding box * based on the current text properties such as font, textSize, and textStyle. Internally, it splits * the text into individual lines (if line breaks are present) and computes the bounding box for each * line using the renderer’s measurement functions. The final width is determined as the maximum width * among all these lines. * * For example, if the text contains multiple lines due to wrapping or explicit line breaks, textWidth() * will return the width of the longest line. * * **Note:** In p5.js 2.0+, leading and trailing spaces are ignored. * `textWidth(" Hello ")` returns the same width as `textWidth("Hello")`. * * @method textWidth * @for p5 * @param {String} text The text to measure * @returns {Number} The width of the text * * @example * function setup() { * createCanvas(200, 200); * background(220); * * // Set text size and alignment * textSize(48); * textAlign(LEFT, TOP); * * let myText = "Hello"; * * // Calculate the width of the text * let tw = textWidth(myText); * * // Draw the text on the canvas * fill(0); * text(myText, 50, 50); * * // Display the text width below * noStroke(); * fill(0); * textSize(20); * text("Text width: " + tw, 10, 150); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textSize(28); * strokeWeight(0.5); * * // Calculate the text width. * let s = 'yoyo'; * let w = textWidth(s); * * // Display the text. * text(s, 22, 55); * * // Underline the text. * line(22, 55, 22 + w, 55); * * describe('The word "yoyo" underlined.'); * } * * @example * function setup() { * createCanvas(200, 160); * background(235); * noLoop(); * * textSize(18); * textAlign(LEFT, TOP); * * const x = 12, h = 24; * const s1 = 'Hello'; * const s2 = 'Hello '; // 2 trailing spaces * const s3 = 'Hello '; // many trailing spaces * * // draw text * fill(0); * text(s1, x, 12); * text(s2, x, 56); * text(s3, x, 100); * * // measure and draw tight boxes (all same width) * noFill(); stroke(255, 0, 0); * const w1 = textWidth(s1); * const w2 = textWidth(s2); * const w3 = textWidth(s3); * rect(x, 10, w1, h); * rect(x, 54, w2, h); * rect(x, 98, w3, h); * * // small captions show the actual strings (spaces as ·) * textSize(10); noStroke(); fill(30); * text('"' + s1.replace(/ /g, '·') + '" w=' + w1.toFixed(1), x, 10 + h + 2); * text('"' + s2.replace(/ /g, '·') + '" w=' + w2.toFixed(1), x, 54 + h + 2); * text('"' + s3.replace(/ /g, '·') + '" w=' + w3.toFixed(1), x, 98 + h + 2); * * describe('Three lines: Hello with 0, 2, and many trailing spaces. Red boxes use textWidth and are identical. Captions show spaces as dots.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textSize(28); * strokeWeight(0.5); * * // Calculate the text width. * // "\n" starts a new line. * let s = 'yo\nyo'; * let w = textWidth(s); * * // Display the text. * text(s, 22, 55); * * // Underline the text. * line(22, 55, 22 + w, 55); * * describe('The word "yo" written twice, one copy beneath the other. The words are divided by a horizontal line.'); * } */ /** * Sets the style for wrapping text when * text() is called. * * The parameter, `style`, can be one of the following values: * * `WORD` starts new lines of text at spaces. If a string of text doesn't * have spaces, it may overflow the text box and the canvas. This is the * default style. * * `CHAR` starts new lines as needed to stay within the text box. * * `textWrap()` only works when the maximum width is set for a text box. For * example, calling `text('Have a wonderful day', 0, 10, 100)` sets the * maximum width to 100 pixels. * * Calling `textWrap()` without an argument returns the current style. * * @method textWrap * @for p5 * * @param {WORD|CHAR} style The wrapping style to use * @returns {CHAR|WORD} If no arguments are provided, the current wrapping style * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textSize(20); * textWrap(WORD); * * // Display the text. * text('Have a wonderful day', 0, 10, 100); * * describe('The text "Have a wonderful day" written across three lines.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textSize(20); * textWrap(CHAR); * * // Display the text. * text('Have a wonderful day', 0, 10, 100); * * describe('The text "Have a wonderful day" written across two lines.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textSize(20); * textWrap(CHAR); * * // Display the text. * text('祝你有美好的一天', 0, 10, 100); * * describe('The text "祝你有美好的一天" written across two lines.'); * } */ /** * @method textWrap * @for p5 * @returns {CHAR|WORD} The current wrapping style */ /** * Computes the tight bounding box for a block of text. * * The `textBounds()` function calculates the precise pixel boundaries that enclose * the rendered text based on the current text properties (such as font, textSize, textStyle, and * alignment). If the text spans multiple lines (due to line breaks or wrapping), the function * measures each line individually and then aggregates these measurements into a single bounding box. * The resulting object contains the x and y coordinates along with the width (w) and height (h) * of the text block. * * @method textBounds * @for p5 * * @param {String} str - The text string to measure. * @param {Number} x - The x-coordinate where the text is drawn. * @param {Number} y - The y-coordinate where the text is drawn. * @param {Number} [width] - (Optional) The maximum width available for the text block. * When specified, the text may be wrapped to fit within this width. * @param {Number} [height] - (Optional) The maximum height available for the text block. * Any lines exceeding this height will be truncated. * @returns {Object} An object with properties `x`, `y`, `w`, and `h` that represent the tight * bounding box of the rendered text. * * @example * function setup() { * createCanvas(300, 200); * background(220); * * // Set up text properties for clarity * textSize(32); * textAlign(LEFT, TOP); * * let txt = "Hello, World!"; * // Compute the bounding box for the text starting at (50, 50) * let bounds = textBounds(txt, 50, 50); * * // Draw the text * fill(0); * text(txt, 50, 50); * * // Draw the computed bounding box in red to visualize the measured area * noFill(); * stroke('red'); * rect(bounds.x, bounds.y, bounds.w, bounds.h); * } */ /** * Sets or gets the text drawing direction. * * The textDirection() function allows you to specify the direction in which text is * rendered on the canvas. When provided with a direction parameter (such as "ltr" for * left-to-right, "rtl" for right-to-left, or "inherit"), it updates the renderer's state with that * value and applies the new setting. When called without any arguments, it returns the current text * direction. This function is particularly useful for rendering text in languages with different * writing directions. * * @method textDirection * @for p5 * * @param {String} direction - The text direction to set ("ltr", "rtl", or "inherit"). * @returns {String} If no arguments are provided, the current text direction, either "ltr", "rtl", or "inherit" * * @example * function setup() { * createCanvas(300, 300); * background(240); * * textSize(32); * textFont("Georgia"); * textAlign(LEFT, TOP); * * // Set text direction to right-to-left and draw Arabic text. * textDirection("rtl"); * fill(0); * text("مرحبًا!", 50, 50); * * // Set text direction to left-to-right and draw English text. * textDirection("ltr"); * text("Hello, p5.js!", 50, 150); * * // Display the current text direction. * textSize(16); * fill(50); * textAlign(LEFT, TOP); * text("Current textDirection: " + textDirection(), 50, 250); * } */ /** * @method textDirection * @for p5 * @returns {String} The current text direction, either "ltr", "rtl", or "inherit" */ /** * Sets or gets a single text property for the renderer. * * The `textProperty()` function allows you to set or retrieve a single text-related property, * such as `textAlign`, `textBaseline`, `fontStyle`, or any other property * that may be part of the renderer's state, its drawing context, or the canvas style. * * When called with a `prop` and a `value`, the function sets the property by checking * for its existence in the renderer's state, the drawing context, or the canvas style. If the property is * successfully modified, the function applies the updated text properties. If called with only the * `prop` parameter, the function returns the current value of that property. * * @method textProperty * @for p5 * * @param {String} prop - The name of the text property to set or get. * @param value - The value to set for the specified text property. If omitted, the current * value of the property is returned * @returns If no arguments are provided, the current value of the specified text property * * @example * function setup() { * createCanvas(300, 300); * background(240); * * // Set the text alignment to CENTER and the baseline to TOP using textProperty. * textProperty("textAlign", CENTER); * textProperty("textBaseline", TOP); * * // Set additional text properties and draw the text. * textSize(32); * textFont("Georgia"); * fill(0); * text("Hello, World!", width / 2, 50); * * // Retrieve and display the current text properties. * let currentAlign = textProperty("textAlign"); * let currentBaseline = textProperty("textBaseline"); * * textSize(16); * textAlign(LEFT, TOP); * fill(50); * text("Current textAlign: " + currentAlign, 50, 150); * text("Current textBaseline: " + currentBaseline, 50, 170); * } */ /** * @method textProperty * @for p5 * @param {String} prop - The name of the text property to set or get. * @returns The current value of the specified text property */ /** * Gets or sets text properties in batch, similar to calling `textProperty()` * multiple times. * * If an object is passed in, `textProperty(key, value)` will be called for you * on every key/value pair in the object. * * If no arguments are passed in, an object will be returned with all the current * properties. * * @method textProperties * @for p5 * @param {Object} properties An object whose keys are properties to set, and whose * values are what they should be set to. */ /** * @method textProperties * @for p5 * @returns {Object} An object with all the possible properties and their current values. */ /** * Computes a generic (non-tight) bounding box for a block of text. * * The `fontBounds()` function calculates the bounding box for the text based on the * font's intrinsic metrics (such as `fontBoundingBoxAscent` and * `fontBoundingBoxDescent`). Unlike `textBounds()`, which measures the exact * pixel boundaries of the rendered text, `fontBounds()` provides a looser measurement * derived from the font’s default spacing. This measurement is useful for layout purposes where * a consistent approximation of the text's dimensions is desired. * * @method fontBounds * @for p5 * * @param {String} str - The text string to measure. * @param {Number} x - The x-coordinate where the text is drawn. * @param {Number} y - The y-coordinate where the text is drawn. * @param {Number} [width] - (Optional) The maximum width available for the text block. * When specified, the text may be wrapped to fit within this width. * @param {Number} [height] - (Optional) The maximum height available for the text block. * Any lines exceeding this height will be truncated. * @returns {Object} An object with properties `x`, `y`, `w`, and `h` representing the loose * bounding box of the text based on the font's intrinsic metrics. * * @example * function setup() { * createCanvas(300, 200); * background(240); * * textSize(32); * textAlign(LEFT, TOP); * textFont('Georgia'); * * let txt = "Hello, World!"; * // Compute the bounding box based on the font's intrinsic metrics * let bounds = fontBounds(txt, 50, 50); * * fill(0); * text(txt, 50, 50); * * noFill(); * stroke('green'); * rect(bounds.x, bounds.y, bounds.w, bounds.h); * * noStroke(); * fill(50); * textSize(15); * text("Font Bounds: x=" + bounds.x.toFixed(1) + ", y=" + bounds.y.toFixed(1) + * ", w=" + bounds.w.toFixed(1) + ", h=" + bounds.h.toFixed(1), 8, 100); * } */ /** * Returns the loose width of a text string based on the current font. * * The `fontWidth()` function measures the width of the provided text string using * the font's default measurement (i.e., the width property from the text metrics returned by * the browser). Unlike `textWidth()`, which calculates the tight pixel boundaries * of the text glyphs, `fontWidth()` uses the font's intrinsic spacing, which may include * additional space for character spacing and kerning. This makes it useful for scenarios where * an approximate width is sufficient for layout and positioning. * * @method fontWidth * @for p5 * * @param {String} theText - The text string to measure. * @returns {Number} The loose width of the text in pixels. * * @example * function setup() { * createCanvas(300, 200); * background(240); * * textSize(32); * textAlign(LEFT, TOP); * textFont('Georgia'); * * let s = "Hello, World!"; * let fw = fontWidth(s); * * fill(0); * text(s, 50, 50); * * stroke('blue'); * line(50, 90, 50 + fw, 90); * * noStroke(); * fill(50); * textSize(16); * text("Font width: " + fw.toFixed(2) + " pixels", 50, 100); * } */ /** * Returns the loose ascent of the text based on the font's intrinsic metrics. * * The `fontAscent()` function calculates the ascent of the text using the font's * intrinsic metrics (e.g., `fontBoundingBoxAscent`). This value represents the space * above the baseline that the font inherently occupies, and is useful for layout purposes when * an approximate vertical measurement is required. * * @method fontAscent * @for p5 * * @returns {Number} The loose ascent value in pixels. * * @example * function setup() { * createCanvas(300, 300); * background(220); * * textSize(35); * textAlign(LEFT, BASELINE); * textFont('Georgia'); * * let s = "Hello, p5.js!"; * let x = 50, y = 150; * * fill(0); * text(s, x, y); * * // Get the font descent of the current font * let fasc = fontAscent(); * * // Draw a red line at the baseline and a blue line at the ascent position * stroke('red'); * line(x, y, x + 200, y); // Baseline * stroke('blue'); * line(x, y - fasc, x + 200, y - fasc); // Font ascent position * * noStroke(); * fill(0); * textSize(16); * text("fontAscent: " + fasc.toFixed(2) + " pixels", x, y + fdesc + 20); * } */ /** * Returns the loose descent of the text based on the font's intrinsic metrics. * * The `fontDescent()` function calculates the descent of the text using the font's * intrinsic metrics (e.g., `fontBoundingBoxDescent`). This value represents the space * below the baseline that the font inherently occupies, and is useful for layout purposes when * an approximate vertical measurement is required. * * @method fontDescent * @for p5 * * @returns {Number} The loose descent value in pixels. * * @example * function setup() { * createCanvas(300, 300); * background(220); * * textSize(48); * textAlign(LEFT, BASELINE); * textFont('Georgia'); * * let s = "Hello, p5.js!"; * let x = 50, y = 150; * * fill(0); * text(s, x, y); * * // Get the font descent of the current font * let fdesc = fontDescent(); * * // Draw a red line at the baseline and a blue line at the descent position * stroke('red'); * line(x, y, x + 200, y); // Baseline * stroke('blue'); * line(x, y + fdesc, x + 200, y + fdesc); // Font descent position * * noStroke(); * fill(0); * textSize(16); * text("fontDescent: " + fdesc.toFixed(2) + " pixels", x, y + fdesc + 20); * } */ /** * * Sets or gets the current font weight. * * The textWeight() function is used to specify the weight (thickness) of the text. * When a numeric value is provided, it sets the font weight to that value and updates the * rendering properties accordingly (including the "font-variation-settings" on the canvas style). * When called without an argument, it returns the current font weight setting. * * @method textWeight * @for p5 * * @param {Number} weight - The numeric weight value to set for the text. * @returns {Number} If no arguments are provided, the current font weight * * @example * function setup() { * createCanvas(300, 200); * background(240); * * // Set text alignment, size, and font * textAlign(LEFT, TOP); * textSize(20); * textFont("Georgia"); * * // Draw text with a normal weight (lighter appearance) * push(); * textWeight(400); // Set font weight to 400 * fill(0); * text("Normal", 50, 50); * let normalWeight = textWeight(); // Should return 400 * pop(); * * // Draw text with a bold weight (heavier appearance) * push(); * textWeight(900); // Set font weight to 900 * fill(0); * text("Bold", 50, 100); * let boldWeight = textWeight(); // Should return 900 * pop(); * * // Display the current font weight values on the canvas * textSize(16); * fill(50); * text("Normal Weight: " + normalWeight, 150, 52); * text("Bold Weight: " + boldWeight, 150, 100); * } * * @example * let font; * * async function setup() { * createCanvas(100, 100); * font = await loadFont( * 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap' * ); * } * * function draw() { * background(255); * textFont(font); * textAlign(LEFT, TOP); * textSize(35); * textWeight(sin(millis() * 0.002) * 200 + 400); * text('p5*js', 0, 10); * describe('The text p5*js pulsing its weight over time'); * } */ /** * @method textWeight * @for p5 * @returns {Number} The current font weight */ // attach each text func to p5, delegating to the renderer textFunctions.forEach(func => { fn[func] = function (...args) { if (!(func in Renderer.prototype)) { throw Error(`Renderer2D.prototype.${func} is not defined.`); } return this._renderer[func](...args); }; // attach also to p5.Graphics.prototype p5.Graphics.prototype[func] = function (...args) { return this._renderer[func](...args); }; }); const RendererTextProps = { textAlign: { default: fn.LEFT, type: 'Context2d' }, textBaseline: { default: fn.BASELINE, type: 'Context2d' }, textFont: { default: { family: 'sans-serif' } }, textLeading: { default: 15 }, textSize: { default: 12 }, textWrap: { default: fn.WORD }, fontStretch: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded } fontWeight: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded } lineHeight: { default: fn.NORMAL, isShorthand: true }, // line-height: { default: normal | number | length | percentage } fontVariant: { default: fn.NORMAL, isShorthand: true }, // font-variant: { default: normal | small-caps } fontStyle: { default: fn.NORMAL, isShorthand: true }, // font-style: { default: normal | italic | oblique } [was 'textStyle' in v1] direction: { default: 'inherit' } // direction: { default: inherit | ltr | rtl } }; // note: font must be first here otherwise it may reset other properties const ContextTextProps = ['font', 'direction', 'fontKerning', 'fontStretch', 'fontVariantCaps', 'letterSpacing', 'textAlign', 'textBaseline', 'textRendering', 'wordSpacing']; // shorthand font properties that can be set with context2d.font const ShorthandFontProps = Object.keys(RendererTextProps) .filter(p => RendererTextProps[p].isShorthand); // allowable values for font-stretch property for context2d.font const FontStretchKeys = ['ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded']; let contextQueue, cachedDiv; // lazy ////////////////////////////// start API /////////////////////////////// Renderer.prototype.text = function (str, x, y, width, height) { let setBaseline = this.textDrawingContext().textBaseline; // store baseline // adjust {x,y,w,h} properties based on rectMode ({ x, y, width, height } = this._handleRectMode(x, y, width, height)); // parse the lines according to width, height & linebreaks let lines = this._processLines(str, width, height); // add the adjusted positions [x,y] to each line lines = this._positionLines(x, y, width, height, lines); // render each line at the adjusted position lines.forEach(line => this._renderText(line.text, line.x, line.y)); this.textDrawingContext().textBaseline = setBaseline; // restore baseline }; /** * Computes the precise (tight) bounding box for a block of text * @param {String} str - the text to measure * @param {Number} x - the x-coordinate of the text * @param {Number} y - the y-coordinate of the text * @param {Number} width - the max width of the text block * @param {Number} height - the max height of the text block * @returns - a bounding box object for the text block: {x,y,w,h} * @private */ Renderer.prototype.textBounds = function (str, x, y, width, height) { // delegate to _textBoundsSingle for measuring return this._computeBounds( textCoreConstants._TEXT_BOUNDS, str, x, y, width, height ).bounds; }; /** * Computes a generic (non-tight) bounding box for a block of text * @param {String} str - the text to measure * @param {Number} x - the x-coordinate of the text * @param {Number} y - the y-coordinate of the text * @param {Number} width - the max width of the text block * @param {Number} height - the max height of the text block * @returns - a bounding box object for the text block: {x,y,w,h} * @private */ Renderer.prototype.fontBounds = function (str, x, y, width, height) { // delegate to _fontBoundsSingle for measuring return this._computeBounds( textCoreConstants._FONT_BOUNDS, str, x, y, width, height ).bounds; }; /** * Get the width of a text string in pixels (tight bounds) * @param {String} theText * @returns - the width of the text in pixels * @private */ Renderer.prototype.textWidth = function (theText) { let lines = this._processLines(theText); // return the max width of the lines (using tight bounds) return Math.max(...lines.map(l => this._textWidthSingle(l))); }; /** * Get the width of a text string in pixels (loose bounds) * @param {String} theText * @returns - the width of the text in pixels * @private */ Renderer.prototype.fontWidth = function (theText) { // return the max width of the lines (using loose bounds) let lines = this._processLines(theText); return Math.max(...lines.map(l => this._fontWidthSingle(l))); }; /** * @param {*} txt - optional text to measure, if provided will be * used to compute the ascent, otherwise the font's ascent will be used * @returns - the ascent of the text * @private */ Renderer.prototype.textAscent = function (txt = '') { if (!txt.length) return this.fontAscent(); return this.textDrawingContext().measureText(txt).actualBoundingBoxAscent; }; /** * @returns - returns the ascent for the current font * @private */ Renderer.prototype.fontAscent = function () { return this.textDrawingContext().measureText('_').fontBoundingBoxAscent; }; /** * @param {*} txt - optional text to measure, if provided will * be used to compute the descent, otherwise the font's descent will be used * @returns - the descent of the text * @private */ Renderer.prototype.textDescent = function (txt = '') { if (!txt.length) return this.fontDescent(); return this.textDrawingContext().measureText(txt).actualBoundingBoxDescent; }; Renderer.prototype.fontDescent = function () { return this.textDrawingContext().measureText('_').fontBoundingBoxDescent; }; // setters/getters for text properties ////////////////////////// Renderer.prototype.textAlign = function (h, v) { // the setter if (typeof h !== 'undefined') { this.states.setValue('textAlign', h); if (typeof v !== 'undefined') { if (v === fn.CENTER) { v = textCoreConstants._CTX_MIDDLE; } this.states.setValue('textBaseline', v); } return this._applyTextProperties(); } // the getter return { horizontal: this.states.textAlign, vertical: this.states.textBaseline }; }; Renderer.prototype._currentTextFont = function () { return this.states.textFont.font || this.states.textFont.family; }; /** * Set the font and [size] and [options] for rendering text * @param {p5.Font | string} font - the font to use for rendering text * @param {Number} size - the size of the text, can be a number or a css-style string * @param {Object} options - additional options for rendering text, see FontProps * @private */ Renderer.prototype.textFont = function (font, size, options) { if (arguments.length === 0) { return this._currentTextFont(); } let family = font; // do we have a custon loaded font ? if (font instanceof p5.Font) { family = font.face.family; } else if (font.data instanceof Uint8Array) { family = font.name.fontFamily; if (font.name?.fontSubfamily) { family += '-' + font.name.fontSubfamily; } } else if (typeof font === 'string') { // direct set the font-string if it contains size if (typeof size === 'undefined' && /[.0-9]+(%|em|p[xt])/.test(family)) { //console.log('direct set font-string: ', family); ({ family, size } = this._directSetFontString(family)); } } if (typeof family !== 'string') throw Error('null font in textFont()'); // handle two-arg case: textFont(font, options) if (arguments.length === 2 && typeof size === 'object') { options = size; size = undefined; } // update font properties in this.states this.states.setValue('textFont', { font, family, size }); // convert/update the size in this.states if (typeof size !== 'undefined') { this._setTextSize(size); } // apply any options to this.states if (typeof options === 'object') { this.textProperties(options); } return this._applyTextProperties(); }; Renderer.prototype._directSetFontString = function (font, debug = 0) { if (debug) console.log('_directSetFontString"' + font + '"'); let defaults = ShorthandFontProps.reduce((props, p) => { props[p] = RendererTextProps[p].default; return props; }, {}); let el = this._cachedDiv(defaults); el.style.font = font; let style = getComputedStyle(el); ShorthandFontProps.forEach(prop => { this.states[prop] = style[prop]; if (debug) console.log(' this.states.' + prop + '="' + style[prop] + '"'); }); return { family: style.fontFamily, size: style.fontSize }; }; Renderer.prototype.textLeading = function (leading) { // the setter if (typeof leading === 'number') { this.states.setValue('leadingSet', true); this.states.setValue('textLeading', leading); return this._applyTextProperties(); } // the getter return this.states.textLeading; }; Renderer.prototype.textWeight = function (weight) { // the setter if (typeof weight === 'number') { this.states.setValue('fontWeight', weight); this._applyTextProperties(); // Safari works without weight set in the canvas style attribute, and actually // has buggy behavior if it is present, using the wrong weight when drawing // multiple times with different weights if (!p5.prototype._isSafari()) { this._setCanvasStyleProperty('font-variation-settings', `"wght" ${weight}`); } return; } // the getter return this.states.fontWeight; }; /** * @param {*} size - the size of the text, can be a number or a css-style string * @private */ Renderer.prototype.textSize = function (size) { // the setter if (typeof size !== 'undefined') { this._setTextSize(size); return this._applyTextProperties(); } // the getter return this.states.textSize; }; Renderer.prototype.textStyle = function (style) { // the setter if (typeof style !== 'undefined') { this.states.setValue('fontStyle', style); return this._applyTextProperties(); } // the getter return this.states.fontStyle; }; Renderer.prototype.textWrap = function (wrapStyle) { if (wrapStyle === fn.WORD || wrapStyle === fn.CHAR) { this.states.setValue('textWrap', wrapStyle); // no need to apply text properties here as not a context property return this._pInst; } return this.states.textWrap; }; Renderer.prototype.textDirection = function (direction) { if (typeof direction !== 'undefined') { this.states.setValue('direction', direction); return this._applyTextProperties(); } return this.states.direction; }; /** * Sets/gets a single text property for the renderer (eg. fontStyle, fontStretch, etc.) * The property to be set can be a mapped or unmapped property on `this.states` or a property * on `this.textDrawingContext()` or on `this.canvas.style` * The property to get can exist in `this.states` or `this.textDrawingContext()` or `this.canvas.style` * @private */ Renderer.prototype.textProperty = function (prop, value, opts) { let modified = false, debug = opts?.debug || false; // getter: return option from this.states or this.textDrawingContext() if (typeof value === 'undefined') { let props = this.textProperties(); if (prop in props) return props[prop]; throw Error('Unknown text option "' + prop + '"'); // FES? } // set the option in this.states if it exists if (prop in this.states && this.states[prop] !== value) { this.states[prop] = value; modified = true; if (debug) { console.log('this.states.' + prop + '="' + options[prop] + '"'); } } // does it exist in CanvasRenderingContext2D ? else if (prop in this.textDrawingContext()) { this._setContextProperty(prop, value, debug); modified = true; } // does it exist in the canvas.style ? else if (prop in this.textCanvas().style) { this._setCanvasStyleProperty(prop, value, debug); modified = true; } else { console.warn('Ignoring unknown text option: "' + prop + '"\n'); // FES? } return modified ? this._applyTextProperties() : this._pInst; }; /** * Batch set/get text properties for the renderer. * The properties can be either on `states` or `drawingContext` * @private */ Renderer.prototype.textProperties = function (properties) { // setter if (typeof properties !== 'undefined') { Object.keys(properties).forEach(opt => { this.textProperty(opt, properties[opt]); }); return this._pInst; } // getter: get props from drawingContext let context = this.textDrawingContext(); properties = ContextTextProps.reduce((props, p) => { props[p] = context[p]; return props; }, {}); // add renderer props Object.keys(RendererTextProps).forEach(p => { if (RendererTextProps[p]?.type === 'Context2d') { properties[p] = context[p]; } else { // a renderer.states property if (p === 'textFont') { // avoid circular ref. inside textFont let current = this._currentTextFont(); if (typeof current === 'object' && '_pInst' in current) { current = Object.assign({}, current); delete current._pInst; } properties[p] = current; } else { properties[p] = this.states[p]; } } }); return properties; }; Renderer.prototype.textMode = function () { /* no-op for processing api */ }; /////////////////////////////// end API //////////////////////////////// Renderer.prototype._currentTextFont = function () { return this.states.textFont.font || this.states.textFont.family; }; /* Compute the bounds for a block of text based on the specified measure function, either _textBoundsSingle or _fontBoundsSingle * @private */ Renderer.prototype._computeBounds = function ( type, str, x, y, width, height, opts ) { let context = this.textDrawingContext(); let setBaseline = context.textBaseline; let { textLeading, textAlign } = this.states; // adjust width, height based on current rectMode ({ width, height } = this._rectModeAdjust(x, y, width, height)); // parse the lines according to the width & linebreaks let lines = this._processLines(str, width, height); // get the adjusted positions [x,y] for each line let boxes = lines.map((line, i) => this[type].bind(this) (line, x, y + i * textLeading)); if (lines.length > 1 && typeof width !== 'undefined') { // fix for #7984 // adjust the bounding boxes for horizontal text alignment in 2d // the WebGL mode version does additional alignment adjustments boxes.forEach(bb => bb.x += p5.Renderer2D.prototype._xAlignOffset.call(this, textAlign, width)); } // adjust the bounding boxes for vertical text alignment in 2d // the WebGL mode version does additional alignment adjustments p5.Renderer2D.prototype._yAlignOffset.call(this, boxes, height || 0); // fix for #7984 // get the bounds for the text block let bounds = boxes[0]; if (lines.length > 1) { // get the bounds for the multi-line text block bounds = this._aggregateBounds(boxes); // align the multi-line bounds if (!opts?.ignoreRectMode) { this._rectModeAlign(bounds, width || 0, height || 0); } } context.textBaseline = setBaseline; // restore baseline return { bounds, lines }; }; /* Adjust width, height of bounds based on current rectMode * @private */ Renderer.prototype._rectModeAdjust = function (x, y, width, height) { if (typeof width !== 'undefined') { switch (this.states.rectMode) { case fn.CENTER: break; case fn.CORNERS: width -= x; height -= y; break; case fn.RADIUS: width *= 2; height *= 2; break; } } return { x, y, width, height }; }; /* Attempts to set a property directly on the canvas.style object * @private */ Renderer.prototype._setCanvasStyleProperty = function (opt, val, debug) { let value = val.toString(); // ensure its a string if (debug) console.log('canvas.style.' + opt + '="' + value + '"'); // handle variable fonts options if (opt === FontVariationSettings) { this._handleFontVariationSettings(value); } // lets try to set it on the canvas style this.textCanvas().style[opt] = value; // check if the value was set successfully if (this.textCanvas().style[opt] !== value) ; }; /* Parses the fontVariationSettings string and sets the font properties, only font-weight working consistently across browsers at present * @private */ Renderer.prototype._handleFontVariationSettings = function ( value, debug = false ) { // check if the value is a string or an object if (typeof value === 'object') { value = Object.keys(value).map(k => k + ' ' + value[k]).join(', '); } let values = value.split(CommaDelimRe); values.forEach(v => { v = v.replace(/["']/g, ''); // remove quotes let matches = VariableAxesRe.exec(v); //console.log('matches: ', matches); if (matches && matches.length) { let axis = matches[0]; // get the value to 3 digits of precision with no trailing zeros let val = parseFloat(parseFloat(v.replace(axis, '').trim()).toFixed(3)); switch (axis) { case 'wght': if (debug) console.log('setting font-weight=' + val); // manually set the font-weight via the font string if (this.states.fontWeight !== val) this.textWeight(val); return val; case 'wdth': break; case 'ital': if (debug) console.log('setting font-style=' + (val ? 'italic' : 'normal')); break; case 'slnt': if (debug) console.log('setting font-style=' + (val ? 'oblique' : 'normal')); break; case 'opsz': if (debug) console.log('setting font-optical-size=' + val); break; } } }); }; /* For properties not directly managed by the renderer in this.states we check if it has a mapping to a property in this.states Otherwise, add the property to the context-queue for later application */ Renderer.prototype._setContextProperty = function (prop, val, debug = false) { // check if the value is actually different, else short-circuit if (this.textDrawingContext()[prop] === val) { return this._pInst; } // otherwise, we will set the property directly on the `this.textDrawingContext()` // by adding [property, value] to context-queue for later application (contextQueue ??= []).push([prop, val]); if (debug) console.log('queued context2d.' + prop + '="' + val + '"'); }; /* Adjust parameters (x,y,w,h) based on current rectMode */ Renderer.prototype._handleRectMode = function (x, y, width, height) { let rectMode = this.states.rectMode; if (typeof width !== 'undefined') { switch (rectMode) { case fn.RADIUS: width *= 2; x -= width / 2; if (typeof height !== 'undefined') { height *= 2; y -= height / 2; } break; case fn.CENTER: x -= width / 2; if (typeof height !== 'undefined') { y -= height / 2; } break; case fn.CORNERS: width -= x; if (typeof height !== 'undefined') { height -= y; } break; } } return { x, y, width, height }; }; /* Get the computed font-size in pixels for a given size string @param {String} size - the font-size string to compute @returns {number} - the computed font-size in pixels * @private */ Renderer.prototype._fontSizePx = function ( theSize, { family } = this.states.textFont ) { const isNumString = num => !isNaN(num) && num.trim() !== ''; // check for a number in a string, eg '12' if (isNumString(theSize)) { return parseFloat(theSize); } let ele = this._cachedDiv({ fontSize: theSize }); ele.style.fontSize = theSize; ele.style.fontFamily = family; let fontSizeStr = getComputedStyle(ele).fontSize; let fontSize = parseFloat(fontSizeStr); if (typeof fontSize !== 'number') { throw Error('textSize: invalid font-size'); } return fontSize; }; Renderer.prototype._cachedDiv = function (props) { if (typeof cachedDiv === 'undefined') { let ele = document.createElement('div'); ele.ariaHidden = 'true'; ele.style.display = 'none'; Object.entries(props).forEach(([prop, val]) => { ele.style[prop] = val; }); this.textCanvas().appendChild(ele); cachedDiv = ele; } return cachedDiv; }; /* Aggregate the bounding boxes of multiple lines of text @param {Array} bboxes - the bounding boxes to aggregate @returns {object} - the aggregated bounding box * @private */ Renderer.prototype._aggregateBounds = function (bboxes) { // loop over the bounding boxes to get the min/max x/y values let minX = Math.min(...bboxes.map(b => b.x)); let minY = Math.min(...bboxes.map(b => b.y)); let maxY = Math.max(...bboxes.map(b => b.y + b.h)); let maxX = Math.max(...bboxes.map(b => b.x + b.w)); return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; }; // Renderer.prototype._aggregateBounds = function (tx, ty, bboxes) { // let x = Math.min(...bboxes.map(b => b.x)); // let y = Math.min(...bboxes.map(b => b.y)); // // the width is the max of the x-offset + the box width // let w = Math.max(...bboxes.map(b => (b.x - tx) + b.w)); // let h = bboxes[bboxes.length - 1].y - bboxes[0].y + bboxes[bboxes.length - 1].h; // return { x, y, w, h }; // }; /* Process the text string to handle line-breaks and text wrapping @param {String} str - the text to process @param {Number} width - the width to wrap the text to @returns {array} - the processed lines of text * @private */ Renderer.prototype._processLines = function (str, width, height) { if (typeof width !== 'undefined') { // only for text with bounds let drawingContext = this.textDrawingContext(); if (drawingContext.textBaseline === fn.BASELINE) { this.drawingContext.textBaseline = fn.TOP; } } let lines = this._splitOnBreaks(str.toString()); let hasLineBreaks = lines.length > 1; let hasWidth = typeof width !== 'undefined'; let exceedsWidth = hasWidth && lines.some(l => this._textWidthSingle(l) > width); let { textLeading: leading, textWrap } = this.states; //if (!hasLineBreaks && !exceedsWidth) return lines; // a single-line if (hasLineBreaks || exceedsWidth) { if (hasWidth) lines = this._lineate(textWrap, lines, width); } // handle height truncation if (hasWidth && typeof height !== 'undefined') { if (typeof leading === 'undefined') { throw Error('leading is required if height is specified'); } // truncate lines that exceed the height for (let i = 0; i < lines.length; i++) { let lh = leading * (i + 1); if (lh > height) { //console.log('TRUNCATING: ', i, '-', lines.length, '"' + lines.slice(i) + '"'); lines = lines.slice(0, i); break; } } } return lines; }; /* Get the x-offset for text given the width and textAlign property */ Renderer.prototype._xAlignOffset = function (textAlign, width) { switch (textAlign) { case fn.LEFT: return 0; case fn.CENTER: return width / 2; case fn.RIGHT: return width; case textCoreConstants.START: return 0; case textCoreConstants.END: throw new Error('textBounds: END not yet supported for textAlign'); default: return 0; } }; /* Align the bounding box based on the current rectMode setting */ Renderer.prototype._rectModeAlign = function (bb, width, height) { if (typeof width !== 'undefined') { switch (this.states.rectMode) { case fn.CENTER: bb.x -= (width - bb.w) / 2; bb.y -= (height - bb.h) / 2; break; case fn.CORNERS: bb.w += bb.x; bb.h += bb.y; break; case fn.RADIUS: bb.x -= (width - bb.w) / 2; bb.y -= (height - bb.h) / 2; bb.w /= 2; bb.h /= 2; break; } return bb; } }; Renderer.prototype._rectModeAlignRevert = function (bb, width, height) { if (typeof width !== 'undefined') { switch (this.states.rectMode) { case fn.CENTER: bb.x += (width - bb.w) / 2; bb.y += (height - bb.h) / 2; break; case fn.CORNERS: bb.w -= bb.x; bb.h -= bb.y; break; case fn.RADIUS: bb.x += (width - bb.w) / 2; bb.y += (height - bb.h) / 2; bb.w *= 2; bb.h *= 2; break; } return bb; } }; /* Get the (tight) width of a single line of text */ Renderer.prototype._textWidthSingle = function (s) { let metrics = this.textDrawingContext().measureText(s); let abl = metrics.actualBoundingBoxLeft; let abr = metrics.actualBoundingBoxRight; return abr + abl; }; /* Get the (loose) width of a single line of text as specified by the font */ Renderer.prototype._fontWidthSingle = function (s) { return this.textDrawingContext().measureText(s).width; }; /* Get the (tight) bounds of a single line of text based on its actual bounding box */ Renderer.prototype._textBoundsSingle = function (s, x = 0, y = 0) { let metrics = this.textDrawingContext().measureText(s); let asc = metrics.actualBoundingBoxAscent; let desc = metrics.actualBoundingBoxDescent; let abl = metrics.actualBoundingBoxLeft; let abr = metrics.actualBoundingBoxRight; return { x: x - abl, y: y - asc, w: abr + abl, h: asc + desc }; }; /* Get the (loose) bounds of a single line of text based on its font's bounding box */ Renderer.prototype._fontBoundsSingle = function (s, x = 0, y = 0) { let metrics = this.textDrawingContext().measureText(s); let asc = metrics.fontBoundingBoxAscent; let desc = metrics.fontBoundingBoxDescent; x -= this._xAlignOffset(this.states.textAlign, metrics.width); return { x, y: y - asc, w: metrics.width, h: asc + desc }; }; /* Set the textSize property in `this.states` if it has changed @param {number | string} theSize - the font-size to set @returns {boolean} - true if the size was changed, false otherwise */ Renderer.prototype._setTextSize = function (theSize) { if (typeof theSize === 'string') { // parse the size string via computed style, eg '2em' theSize = this._fontSizePx(theSize); } // should be a number now if (typeof theSize === 'number') { // set it in `this.states` if its been changed if (this.states.textSize !== theSize) { this.states.setValue('textSize', theSize); // handle leading here, if not set otherwise if (!this.states.leadingSet) { this.states.setValue('textLeading', this.states.textSize * LeadingScale); } return true; // size was changed } } else { console.warn('textSize: invalid size: ' + theSize); } return false; }; /* Split the lines of text based on the width and the textWrap property @param {Array} lines - the lines of text to split @param {Number} maxWidth - the maximum width of the lines @param {Object} opts - additional options for splitting the lines @returns {array} - the split lines of text * @private */ Renderer.prototype._lineate = function ( textWrap, lines, maxWidth = Infinity, opts = {} ) { let splitter = opts.splitChar ?? (textWrap === fn.WORD ? ' ' : ''); let line, testLine, testWidth, words, newLines = []; for (let lidx = 0; lidx < lines.length; lidx++) { line = ''; words = lines[lidx].split(splitter); for (let widx = 0; widx < words.length; widx++) { testLine = `${line + words[widx]}` + splitter; testWidth = this._textWidthSingle(testLine); if (line.length > 0 && testWidth > maxWidth) { newLines.push(line.trim()); line = `${words[widx]}` + splitter; } else { line = testLine; } } newLines.push(line.trim()); } return newLines; }; /* Split the text into lines based on line-breaks and tabs */ Renderer.prototype._splitOnBreaks = function (s) { if (!s || s.length === 0) return ['']; return s.replace(TabsRe, ' ').split(LinebreakRe); }; /* Parse the font-family string to handle complex names, fallbacks, etc. */ Renderer.prototype._parseFontFamily = function (familyStr) { let parts = familyStr.split(CommaDelimRe); let family = parts.map(part => { part = part.trim(); if ((part.indexOf(' ') > -1 || SpecialCharRe.test(part)) && !QuotedRe.test(part)) { part = `"${part}"`; // quote font names with spaces } return part; }).join(', '); return family; }; Renderer.prototype._applyFontString = function () { /* Create the font-string according to the CSS font-string specification: If font is specified as a shorthand for several font-related properties, then: - it must include values for: and - it may optionally include values for: [, , , , ] Format: - font-style, font-variant and font-weight must precede font-size - font-variant may only specify the values defined in CSS 2.1, that is 'normal' and 'small-caps'. - font-stretch may only be a single keyword value. - line-height must immediately follow font-size, preceded by "/", eg 16px/3. - font-family must be the last value specified. */ let { textFont, textSize, lineHeight, fontStyle, fontWeight, fontVariant } = this.states; let drawingContext = this.textDrawingContext(); let family = this._parseFontFamily(textFont.family); let style = fontStyle !== fn.NORMAL ? `${fontStyle} ` : ''; let weight = fontWeight !== fn.NORMAL ? `${fontWeight} ` : ''; let variant = fontVariant !== fn.NORMAL ? `${fontVariant} ` : ''; let fsize = `${textSize}px` + (lineHeight !== fn.NORMAL ? `/${lineHeight} ` : ' '); let fontString = `${style}${variant}${weight}${fsize}${family}`.trim(); //console.log('fontString="' + fontString + '"'); // set the font string on the context drawingContext.font = fontString; // verify that it was set successfully if (drawingContext.font !== fontString) { let expected = fontString; let actual = drawingContext.font; if (expected !== actual) { //console.warn(`Unable to set font property on context2d. It may not be supported.`); //console.log('Expected "' + expected + '" but got: "' + actual + '"'); // TMP return false; } } return true; }; /* Apply the text properties in `this.states` to the `this.textDrawingContext()` Then apply any properties in the context-queue */ Renderer.prototype._applyTextProperties = function (debug = false) { this._applyFontString(); // set these after the font so they're not overridden let context = this.textDrawingContext(); context.direction = this.states.direction; context.textAlign = this.states.textAlign; context.textBaseline = this.states.textBaseline; // set manually as (still) not fully supported as part of font-string let stretch = this.states.fontStretch; if (FontStretchKeys.includes(stretch) && context.fontStretch !== stretch) { context.fontStretch = stretch; } // apply each property in queue after the font so they're not overridden while (contextQueue?.length) { let [prop, val] = contextQueue.shift(); if (debug) console.log('apply context property "' + prop + '" = "' + val + '"'); context[prop] = val; // check if the value was set successfully if (context[prop] !== val) { console.warn(`Unable to set '${prop}' property on context2d. It may not be supported.`); // FES? console.log('Expected "' + val + '" but got: "' + context[prop] + '"'); } } return this._pInst; }; } if (typeof p5 !== 'undefined') { textCore(p5, p5.prototype); } /* * Creates p5.strands filter shaders for cross-platform compatibility. * * NOTE: These work a little differently than p5.js web editor shaders work! * Firstly, it uses instance mode, so we have to explicitly pass in context * variables in an argument to your callback and as a second argument to `modify`. * Secondly, always manually specify uniform names, as variable names will change * in minified builds. */ function makeFilterShader(renderer, operation, p5) { switch (operation) { case GRAY: return renderer.baseFilterShader().modify(({ p5 }) => { p5.getColor((inputs, canvasContent) => { const tex = p5.getTexture(canvasContent, inputs.texCoord); // weighted grayscale with luminance values const gray = p5.dot(tex.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); return p5.vec4(gray, gray, gray, tex.a); }); }, { p5 }); case INVERT: return renderer.baseFilterShader().modify(({ p5 }) => { p5.getColor((inputs, canvasContent) => { const color = p5.getTexture(canvasContent, inputs.texCoord); const invertedColor = p5.vec3(1.0) - color.rgb; return p5.vec4(invertedColor, color.a); }); }, { p5 }); case THRESHOLD: return renderer.baseFilterShader().modify(({ p5 }) => { const filterParameter = p5.uniformFloat('filterParameter'); p5.getColor((inputs, canvasContent) => { const color = p5.getTexture(canvasContent, inputs.texCoord); // weighted grayscale with luminance values const gray = p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); const threshold = p5.floor(filterParameter * 255.0) / 255; const blackOrWhite = p5.step(threshold, gray); return p5.vec4(p5.vec3(blackOrWhite), color.a); }); }, { p5 }); case POSTERIZE: return renderer.baseFilterShader().modify(({ p5 }) => { const filterParameter = p5.uniformFloat('filterParameter'); const quantize = (color, n) => { // restrict values to N options/bins // and floor each channel to nearest value // // eg. when N = 5, values = 0.0, 0.25, 0.50, 0.75, 1.0 // then quantize (0.1, 0.7, 0.9) -> (0.0, 0.5, 1.0) color = color * n; color = p5.floor(color); color = color / (n - 1.0); return color; }; p5.getColor((inputs, canvasContent) => { const color = p5.getTexture(canvasContent, inputs.texCoord); const restrictedColor = quantize(color.rgb, filterParameter); return p5.vec4(restrictedColor, color.a); }); }, { p5 }); case BLUR: return renderer.baseFilterShader().modify(({ p5 }) => { const radius = p5.uniformFloat('radius'); const direction = p5.uniformVec2('direction'); // This isn't a real Gaussian weight, it's a quadratic weight const quadWeight = (x, e) => { return p5.pow(e - p5.abs(x), 2.0); }; const random = (p) => { let p3 = p5.fract(p.xyx * .1031); p3 += p5.dot(p3, p3.yzx + 33.33); return p5.fract((p3.x + p3.y) * p3.z); }; p5.getColor((inputs, canvasContent) => { const uv = inputs.texCoord; // A reasonable maximum number of samples const maxSamples = 64.0; let numSamples = p5.floor(radius * 7.0); if (p5.mod(numSamples, 2) == 0.0) { numSamples++; } let avg = p5.vec4(0.0); let total = 0.0; // Calculate the spacing to avoid skewing if numSamples > maxSamples let spacing = 1.0; if (numSamples > maxSamples) { spacing = numSamples / maxSamples; numSamples = maxSamples; } const randomOffset = (spacing - 1.0) * p5.mix(-0.5, 0.5, random(uv * inputs.canvasSize)); for (let i = 0; i < numSamples; i++) { const sample = i * spacing - (numSamples - 1.0) * 0.5 * spacing + randomOffset; const sampleCoord = uv + p5.vec2(sample, sample) / inputs.canvasSize * direction; const weight = quadWeight(sample, (numSamples - 1.0) * 0.5 * spacing); const texSample = p5.getTexture(canvasContent, sampleCoord); avg += weight * texSample * p5.vec4( texSample.a, texSample.a, texSample.a, 1 ); total += weight; } const blended = avg / total; return p5.vec4( blended.r / blended.a, blended.g / blended.a, blended.b / blended.a, blended.a ); }); }, { p5 }); case ERODE: return renderer.baseFilterShader().modify(({ p5 }) => { const luma = (color) => { return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); }; p5.getColor((inputs, canvasContent) => { const uv = inputs.texCoord; let minColor = p5.getTexture(canvasContent, uv); let minLuma = luma(minColor); for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { if (x != 0 || y != 0) { const offset = p5.vec2(x, y) * inputs.texelSize; const neighborColor = p5.getTexture(canvasContent, uv + offset); const neighborLuma = luma(neighborColor); if (neighborLuma < minLuma) { minLuma = neighborLuma; minColor = neighborColor; } } } } return minColor; }); }, { p5 }); case DILATE: return renderer.baseFilterShader().modify(({ p5 }) => { const luma = (color) => { return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); }; p5.getColor((inputs, canvasContent) => { const uv = inputs.texCoord; let maxColor = p5.getTexture(canvasContent, uv); let maxLuma = luma(maxColor); for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { if (x != 0 || y != 0) { const offset = p5.vec2(x, y) * inputs.texelSize; const neighborColor = p5.getTexture(canvasContent, uv + offset); const neighborLuma = luma(neighborColor); if (neighborLuma > maxLuma) { maxLuma = neighborLuma; maxColor = neighborColor; } } } } return maxColor; }); }, { p5 }); case OPAQUE: return renderer.baseFilterShader().modify(({ p5 }) => { p5.getColor((inputs, canvasContent) => { const color = p5.getTexture(canvasContent, inputs.texCoord); return p5.vec4(color.rgb, 1.0); }); }, { p5 }); default: throw new Error(`Unknown filter: ${operation}`); } } function getStrokeDefs(shaderConstant) { const STROKE_CAP_ENUM = {}; const STROKE_JOIN_ENUM = {}; let lineDefs = ""; const defineStrokeCapEnum = function (key, val) { lineDefs += shaderConstant(`STROKE_CAP_${key}`, `${val}`, 'u32'); STROKE_CAP_ENUM[constants[key]] = val; }; const defineStrokeJoinEnum = function (key, val) { lineDefs += shaderConstant(`STROKE_JOIN_${key}`, `${val}`, 'u32'); STROKE_JOIN_ENUM[constants[key]] = val; }; // Define constants in line shaders for each type of cap/join, and also record // the values in JS objects defineStrokeCapEnum("ROUND", 0); defineStrokeCapEnum("PROJECT", 1); defineStrokeCapEnum("SQUARE", 2); defineStrokeJoinEnum("ROUND", 0); defineStrokeJoinEnum("MITER", 1); defineStrokeJoinEnum("BEVEL", 2); return { STROKE_CAP_ENUM, STROKE_JOIN_ENUM, lineDefs }; } const { STROKE_CAP_ENUM, STROKE_JOIN_ENUM } = getStrokeDefs(()=>""); class Renderer3D extends Renderer { constructor(pInst, w, h, isMainCanvas, elt) { super(pInst, w, h, isMainCanvas); // Create new canvas this.canvas = this.elt = elt || document.createElement("canvas"); this.contextReady = this.setupContext(); if (this._isMainCanvas) { // for pixel method sharing with pimage this._pInst._curElement = this; this._pInst.canvas = this.canvas; } else { // hide if offscreen buffer by default this.canvas.style.display = "none"; } this.elt.id = "defaultCanvas0"; this.elt.classList.add("p5Canvas"); // Set and return p5.Element this.wrappedElt = new Element(this.elt, this._pInst); // Extend renderer with methods of p5.Element with getters for (const p of Object.getOwnPropertyNames(Element.prototype)) { if (p !== 'constructor' && p[0] !== '_') { Object.defineProperty(this, p, { get() { return this.wrappedElt[p]; } }); } } const dimensions = this._adjustDimensions(w, h); w = dimensions.adjustedWidth; h = dimensions.adjustedHeight; this.width = w; this.height = h; // Set canvas size this.elt.width = w * this._pixelDensity; this.elt.height = h * this._pixelDensity; this.elt.style.width = `${w}px`; this.elt.style.height = `${h}px`; this._updateViewport(); // Attach canvas element to DOM if (this._pInst._userNode) { // user input node case this._pInst._userNode.appendChild(this.elt); } else { //create main element if (document.getElementsByTagName("main").length === 0) { let m = document.createElement("main"); document.body.appendChild(m); } //append canvas to main document.getElementsByTagName("main")[0].appendChild(this.elt); } this.isP3D = true; //lets us know we're in 3d mode // When constructing a new Geometry, this will represent the builder this.geometryBuilder = undefined; // Push/pop state this.states.uModelMatrix = new Matrix(4); this.states.uViewMatrix = new Matrix(4); this.states.uPMatrix = new Matrix(4); this.mainCamera = new Camera(this); if (!this.states.curCamera) { this.states.curCamera = this.mainCamera; } this.states.uPMatrix.set(this.states.curCamera.projMatrix); this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); this.states.enableLighting = false; this.states.ambientLightColors = []; this.states.specularColors = [1, 1, 1]; this.states.directionalLightDirections = []; this.states.directionalLightDiffuseColors = []; this.states.directionalLightSpecularColors = []; this.states.pointLightPositions = []; this.states.pointLightDiffuseColors = []; this.states.pointLightSpecularColors = []; this.states.spotLightPositions = []; this.states.spotLightDirections = []; this.states.spotLightDiffuseColors = []; this.states.spotLightSpecularColors = []; this.states.spotLightAngle = []; this.states.spotLightConc = []; this.states.activeImageLight = null; this.states.curFillColor = [1, 1, 1, 1]; this.states.curAmbientColor = [1, 1, 1, 1]; this.states.curSpecularColor = [0, 0, 0, 0]; this.states.curEmissiveColor = [0, 0, 0, 0]; this.states.curStrokeColor = [0, 0, 0, 1]; this.states.curBlendMode = BLEND; this.states._hasSetAmbient = false; this.states._useSpecularMaterial = false; this.states._useEmissiveMaterial = false; this.states._useNormalMaterial = false; this.states._useShininess = 1; this.states._useMetalness = 0; this.states.tint = [255, 255, 255, 255]; this.states.constantAttenuation = 1; this.states.linearAttenuation = 0; this.states.quadraticAttenuation = 0; this.states._currentNormal = new Vector(0, 0, 1); this.states.drawMode = FILL; this.states._tex = null; this.states.textureMode = IMAGE; this.states.textureWrapX = CLAMP; this.states.textureWrapY = CLAMP; // erasing this._isErasing = false; // simple lines this._simpleLines = false; // clipping this._clipDepths = []; this._isClipApplied = false; this._stencilTestOn = false; this.mixedAmbientLight = []; this.mixedSpecularColor = []; // p5.framebuffer for this are calculated in getDiffusedTexture function this.diffusedTextures = new Map(); // p5.framebuffer for this are calculated in getSpecularTexture function this.specularTextures = new Map(); this.preEraseBlend = undefined; this._cachedFillStyle = [1, 1, 1, 1]; this._cachedStrokeStyle = [0, 0, 0, 1]; this._isBlending = false; this._useLineColor = false; this._useVertexColor = false; this.registerEnabled = new Set(); // Camera this.mainCamera._computeCameraDefaultSettings(); this.mainCamera._setDefaultCamera(); // FilterCamera this.filterCamera = new Camera(this); this.filterCamera._computeCameraDefaultSettings(); this.filterCamera._setDefaultCamera(); // Information about the previous frame's touch object // for executing orbitControl() this.prevTouches = []; // Velocity variable for use with orbitControl() this.zoomVelocity = 0; this.rotateVelocity = new Vector(0, 0); this.moveVelocity = new Vector(0, 0); // Flags for recording the state of zooming, rotation and moving this.executeZoom = false; this.executeRotateAndMove = false; this._drawingFilter = false; this._drawingImage = false; this.specularShader = undefined; this.sphereMapping = undefined; this.diffusedShader = undefined; this._baseFilterShader = undefined; this._defaultLightShader = undefined; this._defaultImmediateModeShader = undefined; this._defaultNormalShader = undefined; this._defaultColorShader = undefined; this.states.userFillShader = undefined; this.states.userStrokeShader = undefined; this.states.userImageShader = undefined; this.states.curveDetail = 1 / 4; // Used by beginShape/endShape functions to construct a p5.Geometry this.shapeBuilder = new ShapeBuilder(this); this.geometryBufferCache = new GeometryBufferCache(this); this.curStrokeCap = ROUND; this.curStrokeJoin = ROUND; // map of texture sources to textures created in this gl context via this.getTexture(src) this.textures = new Map(); // set of framebuffers in use this.framebuffers = new Set(); // stack of active framebuffers this.activeFramebuffers = []; // for post processing step this.states.filterShader = undefined; this.filterLayer = undefined; this.filterLayerTemp = undefined; this.defaultFilterShaders = {}; this.fontInfos = {}; this._curShader = undefined; this.drawShapeCount = 1; this.scratchMat3 = new Matrix(3); // Whether or not to remove degenerate faces from geometry. This is usually // set to false for performance. this._validateFaces = false; this.buffers = { fill: [ new RenderBuffer( 3, "vertices", "vertexBuffer", "aPosition", this, this._vToNArray ), new RenderBuffer( 3, "vertexNormals", "normalBuffer", "aNormal", this, this._vToNArray ), new RenderBuffer( 4, "vertexColors", "colorBuffer", "aVertexColor", this ).default((geometry) => geometry.vertices.flatMap(() => [-1, -1, -1, -1])), new RenderBuffer( 3, "vertexAmbients", "ambientBuffer", "aAmbientColor", this ), new RenderBuffer(2, "uvs", "uvBuffer", "aTexCoord", this, (arr) => arr.flat() ), ], stroke: [ new RenderBuffer( 4, "lineVertexColors", "lineColorBuffer", "aVertexColor", this ).default((geometry) => geometry.lineVertices.flatMap(() => [-1, -1, -1, -1])), new RenderBuffer( 3, "lineVertices", "lineVerticesBuffer", "aPosition", this ), new RenderBuffer( 3, "lineTangentsIn", "lineTangentsInBuffer", "aTangentIn", this ), new RenderBuffer( 3, "lineTangentsOut", "lineTangentsOutBuffer", "aTangentOut", this ), new RenderBuffer(1, "lineSides", "lineSidesBuffer", "aSide", this), ], text: [ new RenderBuffer( 3, "vertices", "vertexBuffer", "aPosition", this, this._vToNArray ), new RenderBuffer(2, "uvs", "uvBuffer", "aTexCoord", this, (arr) => arr.flat() ), ], user: [], }; } //This is helper function to reset the context anytime the attributes //are changed with setAttributes() async _resetContext(options, callback, ctor = Renderer3D) { const w = this.width; const h = this.height; const defaultId = this.canvas.id; const isPGraphics = this._pInst instanceof Graphics; // Preserve existing position and styles before recreation const prevStyle = { position: this.canvas.style.position, top: this.canvas.style.top, left: this.canvas.style.left, }; if (isPGraphics) { // Handle PGraphics: remove and recreate the canvas const pg = this._pInst; pg.canvas.parentNode.removeChild(pg.canvas); pg.canvas = document.createElement("canvas"); const node = pg._pInst._userNode || document.body; node.appendChild(pg.canvas); Element.call(pg, pg.canvas, pg._pInst); // Restore previous width and height pg.width = w; pg.height = h; } else { // Handle main canvas: remove and recreate it let c = this.canvas; if (c) { c.parentNode.removeChild(c); } c = document.createElement("canvas"); c.id = defaultId; // Attach the new canvas to the correct parent node if (this._pInst._userNode) { this._pInst._userNode.appendChild(c); } else { document.body.appendChild(c); } this._pInst.canvas = c; this.canvas = c; // Restore the saved position this.canvas.style.position = prevStyle.position; this.canvas.style.top = prevStyle.top; this.canvas.style.left = prevStyle.left; } const renderer = new ctor( this._pInst, w, h, !isPGraphics, this._pInst.canvas ); this._pInst._renderer = renderer; renderer._applyDefaults(); if (renderer.contextReady) { await renderer.contextReady; } if (typeof callback === "function") { //setTimeout with 0 forces the task to the back of the queue, this ensures that //we finish switching out the renderer setTimeout(() => { callback.apply(window._renderer, options); }, 0); } } remove() { this.wrappedElt.remove(); this.wrappedElt = null; this.canvas = null; this.elt = null; } ////////////////////////////////////////////// // Geometry Building ////////////////////////////////////////////// /** * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added * to the geometry and then returned when * endGeometry() is called. One can also use * buildGeometry() to pass a function that * draws shapes. * * If you need to draw complex shapes every frame which don't change over time, * combining them upfront with `beginGeometry()` and `endGeometry()` and then * drawing that will run faster than repeatedly drawing the individual pieces. * @private */ beginGeometry() { if (this.geometryBuilder) { throw new Error( "It looks like `beginGeometry()` is being called while another p5.Geometry is already being build." ); } this.geometryBuilder = new GeometryBuilder(this); this.geometryBuilder.prevFillColor = this.states.fillColor; this.fill(new Color([-1, -1, -1, -1])); } /** * Finishes creating a new p5.Geometry that was * started using beginGeometry(). One can also * use buildGeometry() to pass a function that * draws shapes. * @private * * @returns {p5.Geometry} The model that was built. */ endGeometry() { if (!this.geometryBuilder) { throw new Error( "Make sure you call beginGeometry() before endGeometry()!" ); } const geometry = this.geometryBuilder.finish(); if (this.geometryBuilder.prevFillColor) { this.fill(this.geometryBuilder.prevFillColor); } else { this.noFill(); } this.geometryBuilder = undefined; return geometry; } /** * Creates a new p5.Geometry that contains all * the shapes drawn in a provided callback function. The returned combined shape * can then be drawn all at once using model(). * * If you need to draw complex shapes every frame which don't change over time, * combining them with `buildGeometry()` once and then drawing that will run * faster than repeatedly drawing the individual pieces. * * One can also draw shapes directly between * beginGeometry() and * endGeometry() instead of using a callback * function. * @param {Function} callback A function that draws shapes. * @returns {p5.Geometry} The model that was built from the callback function. */ buildGeometry(callback) { this.beginGeometry(); callback(); return this.endGeometry(); } ////////////////////////////////////////////// // Shape drawing ////////////////////////////////////////////// beginShape(...args) { super.beginShape(...args); // TODO remove when shape refactor is complete // this.shapeBuilder.beginShape(...args); } curveDetail(d) { if (d === undefined) { return this.states.curveDetail; } else { this.states.setValue("curveDetail", d); } } drawShape(shape) { const visitor = new PrimitiveToVerticesConverter({ curveDetail: this.states.curveDetail, }); shape.accept(visitor); this.shapeBuilder.constructFromContours(shape, visitor.contours); if (this.geometryBuilder) { this.geometryBuilder.addImmediate( this.shapeBuilder.geometry, this.shapeBuilder.shapeMode, { validateFaces: this._validateFaces } ); } else if (this.states.fillColor || this.states.strokeColor) { this._drawGeometry(this.shapeBuilder.geometry, { mode: this.shapeBuilder.shapeMode, count: this.drawShapeCount }); } this.drawShapeCount = 1; } endShape(mode, count) { this.drawShapeCount = count; super.endShape(mode, count); } vertexProperty(...args) { this.currentShape.vertexProperty(...args); } normal(xorv, y, z) { if (xorv instanceof Vector) { this.states.setValue("_currentNormal", xorv); } else { this.states.setValue("_currentNormal", new Vector(xorv, y, z)); } this.updateShapeVertexProperties(); } model(model, count = 1) { if (model.vertices.length > 0) { if (this.geometryBuilder) { this.geometryBuilder.addRetained(model); } else { if (!this.geometryInHash(model.gid)) { model._edgesToVertices(); this._getOrMakeCachedBuffers(model); } this._drawGeometry(model, { count }); } } } _getOrMakeCachedBuffers(geometry) { return this.geometryBufferCache.ensureCached(geometry); } ////////////////////////////////////////////// // Rendering ////////////////////////////////////////////// _drawGeometry(geometry, { mode = TRIANGLES, count = 1 } = {}) { for (const propName in geometry.userVertexProperties) { const prop = geometry.userVertexProperties[propName]; this.buffers.user.push( new RenderBuffer( prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this ) ); } if ( this.states.fillColor && geometry.vertices.length >= 3 && ![LINES, POINTS].includes(mode) ) { this._drawFills(geometry, { mode, count }); } if (this.states.strokeColor && geometry.lineVertices.length >= 1) { this._drawStrokes(geometry, { count }); } this.buffers.user = []; } _drawFills(geometry, { count, mode } = {}) { this._useVertexColor = geometry.vertexColors.length > 0 && !geometry.vertexColors.isDefault; const shader = !this._drawingFilter && this.states.userFillShader ? this.states.userFillShader : this._getFillShader(); shader.bindShader('fill'); this._setGlobalUniforms(shader); this._setFillUniforms(shader); shader.bindTextures(); for (const buff of this.buffers.fill) { buff._prepareBuffer(geometry, shader); } this._prepareUserAttributes(geometry, shader); this._disableRemainingAttributes(shader); this._applyColorBlend( this.states.curFillColor, geometry.hasFillTransparency() ); this._drawBuffers(geometry, { mode, count }); shader.unbindShader(); } _drawStrokes(geometry, { count } = {}) { this._useLineColor = geometry.vertexStrokeColors.length > 0; const shader = this._getStrokeShader(); shader.bindShader('stroke'); this._setGlobalUniforms(shader); this._setStrokeUniforms(shader); shader.bindTextures(); for (const buff of this.buffers.stroke) { buff._prepareBuffer(geometry, shader); } this._prepareUserAttributes(geometry, shader); this._disableRemainingAttributes(shader); this._applyColorBlend( this.states.curStrokeColor, geometry.hasStrokeTransparency() ); this._drawBuffers(geometry, {count}); shader.unbindShader(); } _prepareUserAttributes(geometry, shader) { for (const buff of this.buffers.user) { if (!this._pInst.constructor.disableFriendleErrors) { // Check for the right data size const prop = geometry.userVertexProperties[buff.attr]; if (prop) { const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); if (adjustedLength > geometry.vertices.length) { this._pInst.constructor._friendlyError( `One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, "vertexProperty()" ); } else if (adjustedLength < geometry.vertices.length) { this._pInst.constructor._friendlyError( `One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, "vertexProperty()" ); } } } buff._prepareBuffer(geometry, shader); } } _drawGeometryScaled(model, scaleX, scaleY, scaleZ) { let originalModelMatrix = this.states.uModelMatrix; this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); try { this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); if (this.geometryBuilder) { this.geometryBuilder.addRetained(model); } else { this._drawGeometry(model); } } finally { this.states.setValue("uModelMatrix", originalModelMatrix); } } _update() { // reset model view and apply initial camera transform // (containing only look at info; no projection). this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); this.states.uModelMatrix.reset(); this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone()); this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); // reset light data for new frame. this.states.setValue("ambientLightColors", []); this.states.setValue("specularColors", [1, 1, 1]); this.states.setValue("directionalLightDirections", []); this.states.setValue("directionalLightDiffuseColors", []); this.states.setValue("directionalLightSpecularColors", []); this.states.setValue("pointLightPositions", []); this.states.setValue("pointLightDiffuseColors", []); this.states.setValue("pointLightSpecularColors", []); this.states.setValue("spotLightPositions", []); this.states.setValue("spotLightDirections", []); this.states.setValue("spotLightDiffuseColors", []); this.states.setValue("spotLightSpecularColors", []); this.states.setValue("spotLightAngle", []); this.states.setValue("spotLightConc", []); this.states.setValue("enableLighting", false); //reset tint value for new frame this.states.setValue("tint", [255, 255, 255, 255]); //Clear depth every frame this._resetBuffersBeforeDraw(); } background(...args) { const a0 = args[0]; const isImageLike = a0 != null && typeof a0 === 'object' && typeof a0.width === 'number' && typeof a0.height === 'number' && (a0.canvas != null || a0.elt != null); // WEBGL / 3D: support background(image-like) if (isImageLike) { this._pInst.clear(); this._pInst.push(); this._pInst.resetMatrix(); this._pInst.imageMode(CENTER); this._pInst.image(a0, 0, 0, this._pInst.width, this._pInst.height); this._pInst.pop(); return; } // Default: background(color) const _col = this._pInst.color(...args); this.clear(..._col._getRGBA()); } ////////////////////////////////////////////// // Positioning ////////////////////////////////////////////// get uModelMatrix() { return this.states.uModelMatrix; } get uViewMatrix() { return this.states.uViewMatrix; } get uPMatrix() { return this.states.uPMatrix; } get uMVMatrix() { const m = this.uModelMatrix.copy(); m.mult(this.uViewMatrix); return m; } /** * Get a matrix from world-space to screen-space */ getWorldToScreenMatrix() { const modelMatrix = this.states.uModelMatrix; const viewMatrix = this.states.uViewMatrix; const projectionMatrix = this.states.uPMatrix; const projectedToScreenMatrix = new Matrix(4); projectedToScreenMatrix.scale(this.width, this.height, 1); projectedToScreenMatrix.translate([0.5, 0.5, 0.5]); projectedToScreenMatrix.scale(0.5, -0.5, 0.5); const modelViewMatrix = modelMatrix.copy().mult(viewMatrix); const modelViewProjectionMatrix = modelViewMatrix.mult(projectionMatrix); const worldToScreenMatrix = modelViewProjectionMatrix .mult(projectedToScreenMatrix); return worldToScreenMatrix; } ////////////////////////////////////////////// // COLOR ////////////////////////////////////////////// /** * Basic fill material for geometry with a given color * @param {Number|Number[]|String|p5.Color} v1 gray value, * red or hue value (depending on the current color mode), * or color Array, or CSS color string * @param {Number} [v2] green or saturation value * @param {Number} [v3] blue or brightness value * @param {Number} [a] opacity * @chainable * @example * function setup() { * createCanvas(200, 200, WEBGL); * } * * function draw() { * background(0); * noStroke(); * fill(100, 100, 240); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * box(75, 75, 75); * } * * @alt * black canvas with purple cube spinning */ fill(...args) { super.fill(...args); //see material.js for more info on color blending in webgl // const color = fn.color.apply(this._pInst, arguments); const color = this.states.fillColor; this.states.setValue('curFillColor', color._array); this.states.setValue('drawMode', FILL); this.states.setValue('_useNormalMaterial', false); this.states.setValue('_tex', null); } /** * Basic stroke material for geometry with a given color * @param {Number|Number[]|String|p5.Color} v1 gray value, * red or hue value (depending on the current color mode), * or color Array, or CSS color string * @param {Number} [v2] green or saturation value * @param {Number} [v3] blue or brightness value * @param {Number} [a] opacity * @example * function setup() { * createCanvas(200, 200, WEBGL); * } * * function draw() { * background(0); * stroke(240, 150, 150); * fill(100, 100, 240); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * box(75, 75, 75); * } * * @alt * black canvas with purple cube with pink outline spinning */ stroke(...args) { super.stroke(...args); // const color = fn.color.apply(this._pInst, arguments); this.states.setValue('curStrokeColor', this.states.strokeColor._array); } getCommonVertexProperties() { return { ...super.getCommonVertexProperties(), stroke: this.states.strokeColor, fill: this.states.fillColor, normal: this.states._currentNormal, }; } getSupportedIndividualVertexProperties() { return { textureCoordinates: true, }; } strokeCap(cap) { this.curStrokeCap = cap; } strokeJoin(join) { this.curStrokeJoin = join; } getFilterLayer() { if (!this.filterLayer) { this.filterLayer = new Framebuffer$1(this); } return this.filterLayer; } getFilterLayerTemp() { if (!this.filterLayerTemp) { this.filterLayerTemp = new Framebuffer$1(this); } return this.filterLayerTemp; } matchSize(fboToMatch, target) { if ( fboToMatch.width !== target.width || fboToMatch.height !== target.height ) { fboToMatch.resize(target.width, target.height); } if (fboToMatch.pixelDensity() !== target.pixelDensity()) { fboToMatch.pixelDensity(target.pixelDensity()); } } filter(...args) { let fbo = this.getFilterLayer(); // use internal shader for filter constants BLUR, INVERT, etc let filterParameter = undefined; let operation = undefined; if (typeof args[0] === 'string') { operation = args[0]; let useDefaultParam = operation in filterParamDefaults && args[1] === undefined; filterParameter = useDefaultParam ? filterParamDefaults[operation] : args[1]; // Create and store shader for constants once on initial filter call. // Need to store multiple in case user calls different filters, // eg. filter(BLUR) then filter(GRAY) if (!(operation in this.defaultFilterShaders)) { this.defaultFilterShaders[operation] = this._makeFilterShader(fbo.renderer, operation); } this.states.setValue( 'filterShader', this.defaultFilterShaders[operation] ); } // use custom user-supplied shader else { this.states.setValue('filterShader', args[0]); } // Setting the target to the framebuffer when applying a filter to a framebuffer. const target = this.activeFramebuffer() || this; // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. this.matchSize(fbo, target); fbo.draw(() => this.clear()); // prevent undesirable feedback effects accumulating secretly. let texelSize = [ 1 / (target.width * target.pixelDensity()), 1 / (target.height * target.pixelDensity()), ]; // apply blur shader with multiple passes. if (operation === BLUR) { // Treating 'tmp' as a framebuffer. const tmp = this.getFilterLayerTemp(); // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. this.matchSize(tmp, target); // setup this.push(); this.states.setValue('strokeColor', null); this.blendMode(BLEND); // draw main to temp buffer this.shader(this.states.filterShader); this.states.filterShader.setUniform('texelSize', texelSize); this.states.filterShader.setUniform('canvasSize', [ target.width, target.height, ]); this.states.filterShader.setUniform( 'radius', Math.max(1, filterParameter) ); // Horiz pass: draw `target` to `tmp` tmp.draw(() => { this.states.filterShader.setUniform('direction', [1, 0]); this.states.filterShader.setUniform('tex0', target); this.clear(); this.shader(this.states.filterShader); this.noLights(); this.plane(target.width, target.height); }); // Vert pass: draw `tmp` to `fbo` fbo.draw(() => { this.states.filterShader.setUniform('direction', [0, 1]); this.states.filterShader.setUniform('tex0', tmp); this.clear(); this.shader(this.states.filterShader); this.noLights(); this.plane(target.width, target.height); }); this.pop(); } // every other non-blur shader uses single pass else { fbo.draw(() => { this.states.setValue('strokeColor', null); this.blendMode(BLEND); this.shader(this.states.filterShader); this.states.filterShader.setUniform('tex0', target); this.states.filterShader.setUniform('texelSize', texelSize); this.states.filterShader.setUniform('canvasSize', [ target.width, target.height, ]); // filterParameter uniform only used for POSTERIZE, and THRESHOLD // but shouldn't hurt to always set this.states.filterShader.setUniform('filterParameter', filterParameter); this.noLights(); this.plane(target.width, target.height); }); } // draw fbo contents onto main renderer. this.push(); this.states.setValue('strokeColor', null); this.clear(); this.push(); this.states.setValue('imageMode', CORNER); this.blendMode(BLEND); target.filterCamera._resize(); this.setCamera(target.filterCamera); this.resetMatrix(); this._drawingFilter = true; this.image( fbo, 0, 0, fbo.width, fbo.height, -target.width / 2, -target.height / 2, target.width, target.height ); this._drawingFilter = false; this.clearDepth(); this.pop(); this.pop(); } // Pass this off to the host instance so that we can treat a renderer and a // framebuffer the same in filter() pixelDensity(newDensity) { if (newDensity) { return this._pInst.pixelDensity(newDensity); } return this._pInst.pixelDensity(); } blendMode(mode) { if ( mode === DARKEST || mode === LIGHTEST || mode === ADD || mode === BLEND || mode === SUBTRACT || mode === SCREEN || mode === EXCLUSION || mode === REPLACE || mode === MULTIPLY || mode === REMOVE ) this.states.setValue('curBlendMode', mode); else if ( mode === BURN || mode === OVERLAY || mode === HARD_LIGHT || mode === SOFT_LIGHT || mode === DODGE ) { console.warn( 'BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.' ); } } erase(opacityFill, opacityStroke) { if (!this._isErasing) { this.preEraseBlend = this.states.curBlendMode; this._isErasing = true; this.blendMode(REMOVE); this._cachedFillStyle = this.states.curFillColor.slice(); this.states.setValue('curFillColor', [1, 1, 1, opacityFill / 255]); this._cachedStrokeStyle = this.states.curStrokeColor.slice(); this.states.setValue('curStrokeColor', [1, 1, 1, opacityStroke / 255]); } } noErase() { if (this._isErasing) { // Restore colors this.states.setValue('curFillColor', this._cachedFillStyle.slice()); this.states.setValue('curStrokeColor', this._cachedStrokeStyle.slice()); // Restore blend mode this.states.setValue('curBlendMode', this.preEraseBlend); this.blendMode(this.preEraseBlend); // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode this._isErasing = false; this._applyBlendMode(); } } _applyBlendMode() { // By default, a noop } drawTarget() { return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this; } beginClip(options = {}) { super.beginClip(options); this.drawTarget()._isClipApplied = true; this._applyClip(); this.push(); this.resetShader(); if (this.states.fillColor) this.fill(0, 0); if (this.states.strokeColor) this.stroke(0, 0); } endClip() { this.pop(); this._unapplyClip(); // Mark the depth at which the clip has been applied so that we can clear it // when we pop past this depth this._clipDepths.push(this._pushPopDepth); super.endClip(); } _clearClip() { this._clearClipBuffer(); if (this._clipDepths.length > 0) { this._clipDepths.pop(); } this.drawTarget()._isClipApplied = false; } /** * @private * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings * of the renderer's canvas. It will be created if it does not yet exist, and * reused if it does. */ _getTempFramebuffer() { if (!this._tempFramebuffer) { this._tempFramebuffer = new Framebuffer$1(this, { format: UNSIGNED_BYTE, useDepth: this._pInst._glAttributes.depth, depthFormat: UNSIGNED_INT, antialias: this._pInst._glAttributes.antialias, }); } return this._tempFramebuffer; } ////////////////////////////////////////////// // HASH | for geometry ////////////////////////////////////////////// geometryInHash(gid) { return this.geometryBufferCache.isCached(gid); } /** * [resize description] * @private * @param {Number} w [description] * @param {Number} h [description] */ resize(w, h) { super.resize(w, h); // save canvas properties const props = {}; for (const key in this.drawingContext) { const val = this.drawingContext[key]; if (typeof val !== "object" && typeof val !== "function") { props[key] = val; } } const dimensions = this._adjustDimensions(w, h); w = dimensions.adjustedWidth; h = dimensions.adjustedHeight; this.width = w; this.height = h; this.canvas.width = w * this._pixelDensity; this.canvas.height = h * this._pixelDensity; this.canvas.style.width = `${w}px`; this.canvas.style.height = `${h}px`; this._updateViewport(); this._updateSize(); this.mainCamera._resize(); if (this.states.curCamera !== this.mainCamera) { this.states.curCamera._resize(); } //resize pixels buffer if (typeof this.pixels !== "undefined") { this._createPixelsArray(); } for (const framebuffer of this.framebuffers) { // Notify framebuffers of the resize so that any auto-sized framebuffers // can also update their size this.flushDraw?.(); framebuffer._canvasSizeChanged(); } this.flushDraw?.(); // reset canvas properties for (const savedKey in props) { try { this.drawingContext[savedKey] = props[savedKey]; } catch (err) { // ignore read-only property errors } } } applyMatrix(a, b, c, d, e, f) { this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); if (arguments.length === 16) { // this.states.uModelMatrix.apply(arguments); Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); } else { this.states.uModelMatrix.apply([ a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, e, f, 0, 1, ]); } } /** * [translate description] * @private * @param {Number} x [description] * @param {Number} y [description] * @param {Number} z [description] * @chainable * @todo implement handle for components or vector as args */ translate(x, y, z) { if (x instanceof Vector) { z = x.z; y = x.y; x = x.x; } this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); this.states.uModelMatrix.translate([x, y, z]); return this; } /** * Scales the Model View Matrix by a vector * @private * @param {Number | p5.Vector | Array} x [description] * @param {Number} [y] y-axis scalar * @param {Number} [z] z-axis scalar * @chainable */ scale(x, y, z) { this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); this.states.uModelMatrix.scale(x, y, z); return this; } rotate(rad, axis) { if (typeof axis === "undefined") { return this.rotateZ(rad); } this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); Matrix.prototype.rotate4x4.apply(this.states.uModelMatrix, arguments); return this; } rotateX(rad) { this.rotate(rad, 1, 0, 0); return this; } rotateY(rad) { this.rotate(rad, 0, 1, 0); return this; } rotateZ(rad) { this.rotate(rad, 0, 0, 1); return this; } pop(...args) { if ( this._clipDepths.length > 0 && this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] ) { this._clearClip(); } super.pop(...args); this._applyStencilTestIfClipping(); } resetMatrix() { this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); this.states.uModelMatrix.reset(); this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone()); this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); return this; } ////////////////////////////////////////////// // SHADER ////////////////////////////////////////////// _getStrokeShader() { // select the stroke shader to use const stroke = this.states.userStrokeShader; if (stroke) { return stroke; } return this._getLineShader(); } /* * This method will handle both image shaders and * fill shaders, returning the appropriate shader * depending on the current context (image or shape). */ _getFillShader() { // If drawing an image, check for user-defined image shader and filters if (this._drawingImage) { // Use user-defined image shader if available and no filter is applied if (this.states.userImageShader && !this._drawingFilter) { return this.states.userImageShader; } else { return this._getLightShader(); // Fallback to light shader } } // If user has defined a fill shader, return that else if (this.states.userFillShader) { return this.states.userFillShader; } // Use normal shader if normal material is active else if (this.states._useNormalMaterial) { return this._getNormalShader(); } // Use light shader if lighting or textures are enabled else if (this.states.enableLighting || this.states._tex) { return this._getLightShader(); } // Default to color shader if no other conditions are met return this._getColorShader(); } baseMaterialShader() { return this._getLightShader(); } baseNormalShader() { return this._getNormalShader(); } baseColorShader() { return this._getColorShader(); } baseStrokeShader() { return this._getLineShader(); } /** * @private * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if * the main canvas is the current draw target. */ activeFramebuffer() { return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null; } createFramebuffer(options) { return new Framebuffer$1(this, options); } _setGlobalUniforms(shader) { const modelMatrix = this.states.uModelMatrix; const viewMatrix = this.states.uViewMatrix; const projectionMatrix = this.states.uPMatrix; const modelViewMatrix = modelMatrix.copy().mult(viewMatrix); shader.setUniform( "uPerspective", this.states.curCamera.useLinePerspective ? 1 : 0 ); shader.setUniform("uViewMatrix", viewMatrix.mat4); shader.setUniform("uProjectionMatrix", projectionMatrix.mat4); shader.setUniform("uModelMatrix", modelMatrix.mat4); shader.setUniform("uModelViewMatrix", modelViewMatrix.mat4); if (shader.uniforms.uModelViewProjectionMatrix) { const modelViewProjectionMatrix = modelViewMatrix.copy(); modelViewProjectionMatrix.mult(projectionMatrix); shader.setUniform( "uModelViewProjectionMatrix", modelViewProjectionMatrix.mat4 ); } if (shader.uniforms.uNormalMatrix) { this.scratchMat3.inverseTranspose4x4(modelViewMatrix); shader.setUniform("uNormalMatrix", this.scratchMat3.mat3); } if (shader.uniforms.uModelNormalMatrix) { this.scratchMat3.inverseTranspose4x4(this.states.uModelMatrix); shader.setUniform("uModelNormalMatrix", this.scratchMat3.mat3); } if (shader.uniforms.uCameraNormalMatrix) { this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); shader.setUniform("uCameraNormalMatrix", this.scratchMat3.mat3); } shader.setUniform("uViewport", this._viewport); } _setStrokeUniforms(strokeShader) { // set the uniform values strokeShader.setUniform("uSimpleLines", this._simpleLines); strokeShader.setUniform("uUseLineColor", this._useLineColor); strokeShader.setUniform("uMaterialColor", this.states.curStrokeColor); strokeShader.setUniform("uStrokeWeight", this.states.strokeWeight); strokeShader.setUniform("uStrokeCap", STROKE_CAP_ENUM[this.curStrokeCap]); strokeShader.setUniform( "uStrokeJoin", STROKE_JOIN_ENUM[this.curStrokeJoin] ); } _setFillUniforms(fillShader) { this.mixedSpecularColor = [...this.states.curSpecularColor]; const empty = this._getEmptyTexture(); if (this.states._useMetalness > 0) { this.mixedSpecularColor = this.mixedSpecularColor.map( (mixedSpecularColor, index) => this.states.curFillColor[index] * this.states._useMetalness + mixedSpecularColor * (1 - this.states._useMetalness) ); } // TODO: optimize fillShader.setUniform("uUseVertexColor", this._useVertexColor); fillShader.setUniform("uMaterialColor", this.states.curFillColor); fillShader.setUniform("isTexture", !!this.states._tex); // We need to explicitly set uSampler back to an empty texture here. // In general, we record the last set texture so we can re-apply it // the next time a shader is used. However, the texture() function // works differently and is global p5 state. If the p5 state has // been cleared, we also need to clear the value in uSampler to match. fillShader.setUniform("uSampler", this.states._tex || empty); fillShader.setUniform("uTint", this.states.tint); fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient); fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor); fillShader.setUniform("uSpecularMatColor", this.mixedSpecularColor); fillShader.setUniform("uEmissiveMatColor", this.states.curEmissiveColor); fillShader.setUniform("uSpecular", this.states._useSpecularMaterial); fillShader.setUniform("uEmissive", this.states._useEmissiveMaterial); fillShader.setUniform("uShininess", this.states._useShininess); fillShader.setUniform("uMetallic", this.states._useMetalness); this._setImageLightUniforms(fillShader); fillShader.setUniform("uUseLighting", this.states.enableLighting); const pointLightCount = this.states.pointLightDiffuseColors.length / 3; fillShader.setUniform("uPointLightCount", pointLightCount); fillShader.setUniform( "uPointLightLocation", this.states.pointLightPositions ); fillShader.setUniform( "uPointLightDiffuseColors", this.states.pointLightDiffuseColors ); fillShader.setUniform( "uPointLightSpecularColors", this.states.pointLightSpecularColors ); const directionalLightCount = this.states.directionalLightDiffuseColors.length / 3; fillShader.setUniform("uDirectionalLightCount", directionalLightCount); fillShader.setUniform( "uLightingDirection", this.states.directionalLightDirections ); fillShader.setUniform( "uDirectionalDiffuseColors", this.states.directionalLightDiffuseColors ); fillShader.setUniform( "uDirectionalSpecularColors", this.states.directionalLightSpecularColors ); // TODO: sum these here... let mixedAmbientLight = [0, 0, 0]; for (let i = 0; i < this.states.ambientLightColors.length; i += 3) { for (let off = 0; off < 3; off++) { if (this.states._useMetalness > 0) { mixedAmbientLight[off] += Math.max( 0, this.states.ambientLightColors[i + off] - this.states._useMetalness ); } else { mixedAmbientLight[off] += this.states.ambientLightColors[i + off]; } } } fillShader.setUniform("uAmbientColor", mixedAmbientLight); const spotLightCount = this.states.spotLightDiffuseColors.length / 3; fillShader.setUniform("uSpotLightCount", spotLightCount); fillShader.setUniform("uSpotLightAngle", this.states.spotLightAngle); fillShader.setUniform("uSpotLightConc", this.states.spotLightConc); fillShader.setUniform( "uSpotLightDiffuseColors", this.states.spotLightDiffuseColors ); fillShader.setUniform( "uSpotLightSpecularColors", this.states.spotLightSpecularColors ); fillShader.setUniform("uSpotLightLocation", this.states.spotLightPositions); fillShader.setUniform( "uSpotLightDirection", this.states.spotLightDirections ); fillShader.setUniform( "uConstantAttenuation", this.states.constantAttenuation ); fillShader.setUniform("uLinearAttenuation", this.states.linearAttenuation); fillShader.setUniform( "uQuadraticAttenuation", this.states.quadraticAttenuation ); } // getting called from _setFillUniforms _setImageLightUniforms(shader) { //set uniform values shader.setUniform("uUseImageLight", this.states.activeImageLight != null); // true if (this.states.activeImageLight) { // this.states.activeImageLight has image as a key // look up the texture from the diffusedTexture map let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); shader.setUniform("environmentMapDiffused", diffusedLight); let specularLight = this.getSpecularTexture(this.states.activeImageLight); shader.setUniform("environmentMapSpecular", specularLight); } else { shader.setUniform("environmentMapDiffused", this._getEmptyTexture()); shader.setUniform("environmentMapSpecular", this._getEmptyTexture()); } } /** * @private * Note: DO NOT CALL THIS while in the middle of binding another texture, * since it will change the texture binding in order to allocate the empty * texture! Grab its value beforehand! */ _getEmptyTexture() { if (!this._emptyTexture) { // a plain white texture RGBA, full alpha, single pixel. const im = new Image(1, 1); im.set(0, 0, 255); this._emptyTexture = new Texture(this, im); } return this._emptyTexture; } getTexture(input) { let src = input; if (src instanceof Framebuffer$1) { src = src.color; } const texture = this.textures.get(src); if (texture) { return texture; } const tex = new Texture(this, src); this.textures.set(src, tex); return tex; } ////////////////////////////////////////////// // Buffers ////////////////////////////////////////////// _normalizeBufferData(values, type = Float32Array) { if (!values) return null; if (values instanceof DataArray) { return values.dataArray(); } if (values instanceof type) { return values; } return new type(values); } /////////////////////////////// //// UTILITY FUNCTIONS ////////////////////////////// _arraysEqual(a, b) { const aLength = a.length; if (aLength !== b.length) return false; return a.every((ai, i) => ai === b[i]); } _isTypedArray(arr) { return [ Float32Array, Float64Array, Int16Array, Uint16Array, Uint32Array, ].some((x) => arr instanceof x); } /** * turn a p5.Vector Array into a one dimensional number array * @private * @param {p5.Vector[]} arr an array of p5.Vector * @return {Number[]} a one dimensional array of numbers * [p5.Vector(1, 2, 3), p5.Vector(4, 5, 6)] -> * [1, 2, 3, 4, 5, 6] */ _vToNArray(arr) { return arr.flatMap((item) => [item.x, item.y, item.z]); } /////////////////////////////// //// TEXT SUPPORT METHODS ////////////////////////////// _beforeDrawText() {} _afterDrawText() {} textCanvas() { if (!this._textCanvas) { this._textCanvas = document.createElement('canvas'); this._textCanvas.width = 1; this._textCanvas.height = 1; this._textCanvas.style.display = 'none'; // Has to be added to the DOM for measureText to work properly! this.canvas.parentElement.insertBefore(this._textCanvas, this.canvas); } return this._textCanvas; } textDrawingContext() { if (!this._textDrawingContext) { const textCanvas = this.textCanvas(); this._textDrawingContext = textCanvas.getContext('2d'); } return this._textDrawingContext; } _positionLines(x, y, width, height, lines) { let { textLeading, textAlign } = this.states; const widths = lines.map(line => this._fontWidthSingle(line)); let adjustedX, lineData = new Array(lines.length); let adjustedW = typeof width === 'undefined' ? Math.max(0, ...widths) : width; let adjustedH = typeof height === 'undefined' ? 0 : height; for (let i = 0; i < lines.length; i++) { switch (textAlign) { case textCoreConstants.START: throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT case LEFT: adjustedX = x; break; case CENTER: adjustedX = x + (adjustedW - widths[i]) / 2 - adjustedW / 2 + (width || 0) / 2; break; case RIGHT: adjustedX = x + adjustedW - widths[i] - adjustedW + (width || 0); break; case textCoreConstants.END: throw new Error('textBounds: END not yet supported for textAlign'); default: adjustedX = x; break; } lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; } return this._yAlignOffset(lineData, adjustedH); } _verticalAlignFont = function() { const ctx = this.textDrawingContext(); const metrics = ctx.measureText('X'); return -metrics.alphabeticBaseline || (-metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent); } _yAlignOffset(dataArr, height) { if (typeof height === 'undefined') { throw Error('_yAlignOffset: height is required'); } let { textLeading, textBaseline, textSize} = this.states; let yOff = 0, numLines = dataArr.length; let totalHeight = textSize * numLines + ((textLeading - textSize) * (numLines - 1)); switch (textBaseline) { // drawingContext ? case TOP: yOff = this._verticalAlignFont(); break; case BASELINE: break; case textCoreConstants._CTX_MIDDLE: yOff = (-totalHeight + textSize + (height || 0)) / 2 + this._verticalAlignFont() + this._middleAlignOffset(); break; case BOTTOM: yOff = -(totalHeight - textSize) + (height || 0); break; default: console.warn(`${textBaseline} is not supported in WebGL mode.`); // FES? break; } dataArr.forEach(ele => ele.y += yOff); return dataArr; } _makeFilterShader(renderer, operation) { const p5 = this._pInst; return makeFilterShader(this, operation, p5); } /* * As part of imageLight(): we need to create a texture representing * the diffused light hitting an object from each angle. This will * accumulate light from angles in a hemisphere, weighted according to * how head-on the light angle is. * * This method returns a p5.Framebuffer that stores these values, mapping * an angle to each pixel. This creates and caches textures for reuse, since * creating this texture is somewhat expensive. */ makeDiffusedTexture(input) { // if one already exists for a given input image if (this.diffusedTextures.get(input) != null) { return this.diffusedTextures.get(input); } // if not, only then create one let newFramebuffer; // hardcoded to 200px, because it's going to be blurry and smooth let smallWidth = 200; let width = smallWidth; let height = Math.floor(smallWidth * (input.height / input.width)); newFramebuffer = new Framebuffer$1(this, { width, height, density: 1, }); // create framebuffer is like making a new sketch, all functions on main // sketch it would be available on framebuffer if (!this.diffusedShader) { this.diffusedShader = this._createImageLightShader("diffused"); } newFramebuffer.draw(() => { this.shader(this.diffusedShader); this._setImageLightShaderUniforms(this.diffusedShader, input); this.states.setValue("strokeColor", null); this.noLights(); this.plane(width, height); }); this.diffusedTextures.set(input, newFramebuffer); return newFramebuffer; } getDiffusedTexture(input) { return this.diffusedTextures.get(input); } /* * used in imageLight, * To create a texture from the input non blurry image, if it doesn't already exist * Creating 8 different levels of textures according to different * sizes and storing them in `levels` array * Creating a new Mipmap texture with that `levels` array * Storing the texture for input image in map called `specularTextures` * maps the input Image to a p5.MipmapTexture */ makeSpecularTexture(input) { // check if already exits (there are tex of diff resolution so which one to check) // currently doing the whole array if (this.specularTextures.get(input) != null) { return this.specularTextures.get(input); } // Hardcoded size const size = 512; let tex; let count = Math.floor(Math.log2(size)) + 1; // Actual number of mip levels from size down to 1x1 if (!this.specularShader) { this.specularShader = this._createImageLightShader("specular"); } // Prepare mipmap level accumulator const mipmapData = this._prepareMipmapData(size, count); const framebuffer = new Framebuffer$1(this, { width: size, height: size, density: 1, }); // currently only 8 levels // This loop calculates 8 framebuffers of varying size of canvas // and corresponding different roughness levels. // Roughness increases with the decrease in canvas size, // because rougher surfaces have less detailed/more blurry reflections. let mipLevel = 0; for (let w = size; w >= 1; w /= 2) { framebuffer.resize(w, w); let currCount = Math.log(w) / Math.log(2); let roughness = 1 - currCount / count; framebuffer.draw(() => { this.shader(this.specularShader); this.clear(); this._setImageLightShaderUniforms( this.specularShader, input, roughness, ); this.states.setValue("strokeColor", null); this.noLights(); this.plane(w, w); }); // Accumulate framebuffer content for this mip level this._accumulateMipLevel(framebuffer, mipmapData, mipLevel, w, w); mipLevel++; } // Free the Framebuffer framebuffer.remove(); // Create the final MipmapTexture from accumulated data tex = this._finalizeMipmapTexture(mipmapData); this.specularTextures.set(input, tex); return tex; } getSpecularTexture(input) { return this.specularTextures.get(input); } _getSphereMapping(img) { if (!this.sphereMapping) { const p5 = this._pInst; this.sphereMapping = this.baseFilterShader().modify(({ p5 }) => { const uEnvMap = p5.uniformTexture('uEnvMap'); const uFovY = p5.uniformFloat('uFovY'); const uAspect = p5.uniformFloat('uAspect'); // Hack: we don't have matrix uniforms yet; use three vectors const uN1 = p5.uniformVec3('uN1'); const uN2 = p5.uniformVec3('uN2'); const uN3 = p5.uniformVec3('uN3'); p5.getColor((inputs) => { const uFovX = uFovY * uAspect; const angleY = p5.mix(uFovY/2.0, -uFovY/2.0, inputs.texCoord.y); const angleX = p5.mix(uFovX/2.0, -uFovX/2.0, inputs.texCoord.x); let rotatedNormal = p5.normalize([angleX, angleY, 1]); rotatedNormal = [ // Don't mind me, just doing matrix vector multiplication... p5.dot(rotatedNormal, uN1), p5.dot(rotatedNormal, uN2), p5.dot(rotatedNormal, uN3), ]; const temp = rotatedNormal.z; rotatedNormal.z = rotatedNormal.x; rotatedNormal.x = -temp; const suv = [ p5.atan(rotatedNormal.z, rotatedNormal.x) / (2.0 * p5.PI) + 0.5, 0.5 + 0.5 * (-rotatedNormal.y) ]; return p5.getTexture(uEnvMap, suv); }); }, { p5 }); } this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); this.scratchMat3.invert(this.scratchMat3); // uNMMatrix is 3x3 this.sphereMapping.setUniform("uFovY", this.states.curCamera.cameraFOV); this.sphereMapping.setUniform("uAspect", this.states.curCamera.aspectRatio); // Pass in the normal matrix as three vectors. TODO replace this with // an actual matrix uniform once we have those again. const m = this.scratchMat3.mat3; this.sphereMapping.setUniform("uN1", [m[0], m[3], m[6]]); this.sphereMapping.setUniform("uN2", [m[1], m[4], m[7]]); this.sphereMapping.setUniform("uN3", [m[2], m[5], m[8]]); this.sphereMapping.setUniform("uEnvMap", img); return this.sphereMapping; } /* * Abstract methods to be implemented by specific renderers */ _createImageLightShader(type) { throw new Error( "_createImageLightShader must be implemented by the renderer", ); } _setImageLightShaderUniforms(shader, input, roughness) { shader.setUniform("environmentMap", input); if (roughness !== undefined) { shader.setUniform("roughness", roughness); } } _createMipmapTexture(levels) { throw new Error("_createMipmapTexture must be implemented by the renderer"); } _prepareMipmapData(size, mipLevels) { throw new Error("_prepareMipmapData must be implemented by the renderer"); } _accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) { throw new Error("_accumulateMipLevel must be implemented by the renderer"); } _finalizeMipmapTexture(mipmapData) { throw new Error( "_finalizeMipmapTexture must be implemented by the renderer", ); } remove() { if (this._textCanvas) { this._textCanvas.parentElement.removeChild(this._textCanvas); } super.remove(); } } function renderer3D(p5, fn) { p5.Renderer3D = Renderer3D; } if (typeof p5 !== "undefined") { renderer3D(p5); } /** * @module Shape * @submodule 3D Primitives * @for p5 * @requires core * @requires p5.Geometry */ function primitives3D(p5, fn){ /** * Sets the stroke rendering mode to balance performance and visual features when drawing lines. * * `strokeMode()` offers two modes: * * - `SIMPLE`: Optimizes for speed by disabling caps, joins, and stroke color features. * Use this mode for faster line rendering when these visual details are unnecessary. * - `FULL`: Enables caps, joins, and stroke color for lines. * This mode provides enhanced visuals but may reduce performance due to additional processing. * * Choose the mode that best suits your application's needs to either improve rendering speed or enhance visual quality. * * @method strokeMode * @param {String} mode - The stroke mode to set. Possible values are: * - `'SIMPLE'`: Fast rendering without caps, joins, or stroke color. * - `'FULL'`: Detailed rendering with caps, joins, and stroke color. * * @example * function setup() { * createCanvas(300, 300, WEBGL); * describe('A sphere with red stroke and a red, wavy line on a gray background. The wavy line have caps, joins and colors.'); * } * * function draw() { * background(128); * strokeMode(FULL); // Enables detailed rendering with caps, joins, and stroke color. * push(); * strokeWeight(1); * translate(0, -50, 0); * sphere(50); * pop(); * orbitControl(); * * noFill(); * strokeWeight(15); * stroke('red'); * beginShape(); * bezierOrder(2); // Sets the order of the Bezier curve. * bezierVertex(80, 80); * bezierVertex(50, -40); * bezierVertex(-80, 80); * endShape(); * } * * @example * function setup() { * createCanvas(300, 300, WEBGL); * describe('A sphere with red stroke and a wavy line without full curve decorations without caps and color on a gray background.'); * } * * function draw() { * background(128); * strokeMode(SIMPLE); // Simplifies stroke rendering for better performance. * * // Draw sphere * push(); * strokeWeight(1); * translate(0, -50, 0); * sphere(50); * pop(); * orbitControl(); * * // Draw modified wavy red line * noFill(); * strokeWeight(15); * stroke('red'); * beginShape(); * bezierOrder(2); // Sets the order of the Bezier curve. * bezierVertex(80, 80); * bezierVertex(50, -40); * bezierVertex(-80, 80); * endShape(); * } */ fn.strokeMode = function (mode) { if (mode === undefined) { return this._renderer._simpleLines ? SIMPLE : FULL; } else if (mode === SIMPLE) { this._renderer._simpleLines = true; } else if (mode === FULL) { this._renderer._simpleLines = false; } else { throw Error('no such parameter'); } }; /** * Creates a custom p5.Geometry object from * simpler 3D shapes. * * `buildGeometry()` helps with creating complex 3D shapes from simpler ones * such as sphere(). It can help to make sketches * more performant. For example, if a complex 3D shape doesn’t change while a * sketch runs, then it can be created with `buildGeometry()`. Creating a * p5.Geometry object once and then drawing it * will run faster than repeatedly drawing the individual pieces. * * The parameter, `callback`, is a function with the drawing instructions for * the new p5.Geometry object. It will be called * once to create the new 3D shape. * * See beginGeometry() and * endGeometry() for another way to build 3D * shapes. * * Note: `buildGeometry()` can only be used in WebGL mode. * * @method buildGeometry * @param {Function} callback function that draws the shape. * @returns {p5.Geometry} new 3D shape. * * @example * // Click and drag the mouse to view the scene from different angles. * * let shape; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the p5.Geometry object. * shape = buildGeometry(createShape); * * describe('A white cone drawn on a gray background.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the p5.Geometry object. * noStroke(); * * // Draw the p5.Geometry object. * model(shape); * } * * // Create p5.Geometry object from a single cone. * function createShape() { * cone(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let shape; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the arrow. * shape = buildGeometry(createArrow); * * describe('A white arrow drawn on a gray background.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the arrow. * noStroke(); * * // Draw the arrow. * model(shape); * } * * function createArrow() { * // Add shapes to the p5.Geometry object. * push(); * rotateX(PI); * cone(10); * translate(0, -10, 0); * cylinder(3, 20); * pop(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let shape; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the p5.Geometry object. * shape = buildGeometry(createArrow); * * describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the arrows. * noStroke(); * * // Draw the p5.Geometry object. * model(shape); * * // Translate and rotate the coordinate system. * translate(30, 0, 0); * rotateZ(frameCount * 0.01); * * // Draw the p5.Geometry object again. * model(shape); * } * * function createArrow() { * // Add shapes to the p5.Geometry object. * push(); * rotateX(PI); * cone(10); * translate(0, -10, 0); * cylinder(3, 20); * pop(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let button; * let particles; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a button to reset the particle system. * button = createButton('Reset'); * * // Call resetModel() when the user presses the button. * button.mousePressed(resetModel); * * // Add the original set of particles. * resetModel(); * * describe('A set of white spheres on a gray background. The spheres are positioned randomly. Their positions reset when the user presses the Reset button.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the particles. * noStroke(); * * // Draw the particles. * model(particles); * } * * function resetModel() { * // If the p5.Geometry object has already been created, * // free those resources. * if (particles) { * freeGeometry(particles); * } * * // Create a new p5.Geometry object with random spheres. * particles = buildGeometry(createParticles); * } * * function createParticles() { * for (let i = 0; i < 60; i += 1) { * // Calculate random coordinates. * let x = randomGaussian(0, 20); * let y = randomGaussian(0, 20); * let z = randomGaussian(0, 20); * * push(); * // Translate to the particle's coordinates. * translate(x, y, z); * // Draw the particle. * sphere(5); * pop(); * } * } */ fn.buildGeometry = function(callback) { return this._renderer.buildGeometry(callback); }; /** * Clears a p5.Geometry object from the graphics * processing unit (GPU) memory. * * p5.Geometry objects can contain lots of data * about their vertices, surface normals, colors, and so on. Complex 3D shapes * can use lots of memory which is a limited resource in many GPUs. Calling * `freeGeometry()` can improve performance by freeing a * p5.Geometry object’s resources from GPU memory. * `freeGeometry()` works with p5.Geometry objects * created with beginGeometry() and * endGeometry(), * buildGeometry(), and * loadModel(). * * The parameter, `geometry`, is the p5.Geometry * object to be freed. * * Note: A p5.Geometry object can still be drawn * after its resources are cleared from GPU memory. It may take longer to draw * the first time it’s redrawn. * * Note: `freeGeometry()` can only be used in WebGL mode. * * @method freeGeometry * @param {p5.Geometry} geometry 3D shape whose resources should be freed. * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Geometry object. * beginGeometry(); * cone(); * let shape = endGeometry(); * * // Draw the shape. * model(shape); * * // Free the shape's resources. * freeGeometry(shape); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let button; * let particles; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a button to reset the particle system. * button = createButton('Reset'); * * // Call resetModel() when the user presses the button. * button.mousePressed(resetModel); * * // Add the original set of particles. * resetModel(); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the particles. * noStroke(); * * // Draw the particles. * model(particles); * } * * function resetModel() { * // If the p5.Geometry object has already been created, * // free those resources. * if (particles) { * freeGeometry(particles); * } * * // Create a new p5.Geometry object with random spheres. * particles = buildGeometry(createParticles); * } * * function createParticles() { * for (let i = 0; i < 60; i += 1) { * // Calculate random coordinates. * let x = randomGaussian(0, 20); * let y = randomGaussian(0, 20); * let z = randomGaussian(0, 20); * * push(); * // Translate to the particle's coordinates. * translate(x, y, z); * // Draw the particle. * sphere(5); * pop(); * } * } */ fn.freeGeometry = function(geometry) { this._renderer.geometryBufferCache.freeBuffers(geometry.gid); }; /** * Draws a plane. * * A plane is a four-sided, flat shape with every angle measuring 90˚. It’s * similar to a rectangle and offers advanced drawing features in WebGL mode. * * The first parameter, `width`, is optional. If a `Number` is passed, as in * `plane(20)`, it sets the plane’s width and height. By default, `width` is * 50. * * The second parameter, `height`, is also optional. If a `Number` is passed, * as in `plane(20, 30)`, it sets the plane’s height. By default, `height` is * set to the plane’s `width`. * * The third parameter, `detailX`, is also optional. If a `Number` is passed, * as in `plane(20, 30, 5)` it sets the number of triangle subdivisions to use * along the x-axis. All 3D shapes are made by connecting triangles to form * their surfaces. By default, `detailX` is 1. * * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, * as in `plane(20, 30, 5, 7)` it sets the number of triangle subdivisions to * use along the y-axis. All 3D shapes are made by connecting triangles to * form their surfaces. By default, `detailY` is 1. * * Note: `plane()` can only be used in WebGL mode. * * @method plane * @param {Number} [width] width of the plane. * @param {Number} [height] height of the plane. * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white plane on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the plane. * plane(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white plane on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the plane. * // Set its width and height to 30. * plane(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white plane on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the plane. * // Set its width to 30 and height to 50. * plane(30, 50); * } */ fn.plane = function( width = 50, height = width, detailX = 1, detailY = 1 ) { this._assert3d('plane'); // p5._validateParameters('plane', arguments); this._renderer.plane(width, height, detailX, detailY); return this; }; /** * Draws a box (rectangular prism). * * A box is a 3D shape with six faces. Each face makes a 90˚ with four * neighboring faces. * * The first parameter, `width`, is optional. If a `Number` is passed, as in * `box(20)`, it sets the box’s width and height. By default, `width` is 50. * * The second parameter, `height`, is also optional. If a `Number` is passed, * as in `box(20, 30)`, it sets the box’s height. By default, `height` is set * to the box’s `width`. * * The third parameter, `depth`, is also optional. If a `Number` is passed, as * in `box(20, 30, 40)`, it sets the box’s depth. By default, `depth` is set * to the box’s `height`. * * The fourth parameter, `detailX`, is also optional. If a `Number` is passed, * as in `box(20, 30, 40, 5)`, it sets the number of triangle subdivisions to * use along the x-axis. All 3D shapes are made by connecting triangles to * form their surfaces. By default, `detailX` is 1. * * The fifth parameter, `detailY`, is also optional. If a number is passed, as * in `box(20, 30, 40, 5, 7)`, it sets the number of triangle subdivisions to * use along the y-axis. All 3D shapes are made by connecting triangles to * form their surfaces. By default, `detailY` is 1. * * Note: `box()` can only be used in WebGL mode. * * @method box * @param {Number} [width] width of the box. * @param {Number} [height] height of the box. * @param {Number} [depth] depth of the box. * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white box on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white box on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the box. * // Set its width and height to 30. * box(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white box on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the box. * // Set its width to 30 and height to 50. * box(30, 50); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white box on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the box. * // Set its width to 30, height to 50, and depth to 10. * box(30, 50, 10); * } */ fn.box = function(width, height, depth, detailX, detailY) { this._assert3d('box'); // p5._validateParameters('box', arguments); this._renderer.box(width, height, depth, detailX, detailY); return this; }; /** * Draws a sphere. * * A sphere is a 3D shape with triangular faces that connect to form a round * surface. Spheres with few faces look like crystals. Spheres with many faces * have smooth surfaces and look like balls. * * The first parameter, `radius`, is optional. If a `Number` is passed, as in * `sphere(20)`, it sets the radius of the sphere. By default, `radius` is 50. * * The second parameter, `detailX`, is also optional. If a `Number` is passed, * as in `sphere(20, 5)`, it sets the number of triangle subdivisions to use * along the x-axis. All 3D shapes are made by connecting triangles to form * their surfaces. By default, `detailX` is 24. * * The third parameter, `detailY`, is also optional. If a `Number` is passed, * as in `sphere(20, 5, 2)`, it sets the number of triangle subdivisions to * use along the y-axis. All 3D shapes are made by connecting triangles to * form their surfaces. By default, `detailY` is 16. * * Note: `sphere()` can only be used in WebGL mode. * * @method sphere * @param {Number} [radius] radius of the sphere. Defaults to 50. * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24. * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. * * @chainable * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the sphere. * sphere(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the sphere. * // Set its radius to 30. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the sphere. * // Set its radius to 30. * // Set its detailX to 6. * sphere(30, 6); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the sphere. * // Set its radius to 30. * // Set its detailX to 24. * // Set its detailY to 4. * sphere(30, 24, 4); * } */ fn.sphere = function(radius = 50, detailX = 24, detailY = 16) { this._assert3d('sphere'); // p5._validateParameters('sphere', arguments); this._renderer.sphere(radius, detailX, detailY); return this; }; /** * Draws a cylinder. * * A cylinder is a 3D shape with triangular faces that connect a flat bottom * to a flat top. Cylinders with few faces look like boxes. Cylinders with * many faces have smooth surfaces. * * The first parameter, `radius`, is optional. If a `Number` is passed, as in * `cylinder(20)`, it sets the radius of the cylinder’s base. By default, * `radius` is 50. * * The second parameter, `height`, is also optional. If a `Number` is passed, * as in `cylinder(20, 30)`, it sets the cylinder’s height. By default, * `height` is set to the cylinder’s `radius`. * * The third parameter, `detailX`, is also optional. If a `Number` is passed, * as in `cylinder(20, 30, 5)`, it sets the number of edges used to form the * cylinder's top and bottom. Using more edges makes the top and bottom look * more like circles. By default, `detailX` is 24. * * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, * as in `cylinder(20, 30, 5, 2)`, it sets the number of triangle subdivisions * to use along the y-axis, between cylinder's the top and bottom. All 3D * shapes are made by connecting triangles to form their surfaces. By default, * `detailY` is 1. * * The fifth parameter, `bottomCap`, is also optional. If a `false` is passed, * as in `cylinder(20, 30, 5, 2, false)` the cylinder’s bottom won’t be drawn. * By default, `bottomCap` is `true`. * * The sixth parameter, `topCap`, is also optional. If a `false` is passed, as * in `cylinder(20, 30, 5, 2, false, false)` the cylinder’s top won’t be * drawn. By default, `topCap` is `true`. * * Note: `cylinder()` can only be used in WebGL mode. * * @method cylinder * @param {Number} [radius] radius of the cylinder. Defaults to 50. * @param {Number} [height] height of the cylinder. Defaults to the value of `radius`. * @param {Integer} [detailX] number of edges along the top and bottom. Defaults to 24. * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 1. * @param {Boolean} [bottomCap] whether to draw the cylinder's bottom. Defaults to `true`. * @param {Boolean} [topCap] whether to draw the cylinder's top. Defaults to `true`. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cylinder on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cylinder. * cylinder(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cylinder on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cylinder. * // Set its radius and height to 30. * cylinder(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cylinder on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cylinder. * // Set its radius to 30 and height to 50. * cylinder(30, 50); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white box on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cylinder. * // Set its radius to 30 and height to 50. * // Set its detailX to 5. * cylinder(30, 50, 5); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cylinder on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cylinder. * // Set its radius to 30 and height to 50. * // Set its detailX to 24 and detailY to 2. * cylinder(30, 50, 24, 2); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cylinder on a gray background. Its top is missing.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cylinder. * // Set its radius to 30 and height to 50. * // Set its detailX to 24 and detailY to 1. * // Don't draw its bottom. * cylinder(30, 50, 24, 1, false); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cylinder on a gray background. Its top and bottom are missing.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cylinder. * // Set its radius to 30 and height to 50. * // Set its detailX to 24 and detailY to 1. * // Don't draw its bottom or top. * cylinder(30, 50, 24, 1, false, false); * } */ fn.cylinder = function( radius = 50, height = radius, detailX = 24, detailY = 1, bottomCap = true, topCap = true ) { this._assert3d('cylinder'); // p5._validateParameters('cylinder', arguments); this._renderer.cylinder( radius, height, detailX, detailY, bottomCap, topCap ); return this; }; /** * Draws a cone. * * A cone is a 3D shape with triangular faces that connect a flat bottom to a * single point. Cones with few faces look like pyramids. Cones with many * faces have smooth surfaces. * * The first parameter, `radius`, is optional. If a `Number` is passed, as in * `cone(20)`, it sets the radius of the cone’s base. By default, `radius` is * 50. * * The second parameter, `height`, is also optional. If a `Number` is passed, * as in `cone(20, 30)`, it sets the cone’s height. By default, `height` is * set to the cone’s `radius`. * * The third parameter, `detailX`, is also optional. If a `Number` is passed, * as in `cone(20, 30, 5)`, it sets the number of edges used to form the * cone's base. Using more edges makes the base look more like a circle. By * default, `detailX` is 24. * * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, * as in `cone(20, 30, 5, 7)`, it sets the number of triangle subdivisions to * use along the y-axis connecting the base to the tip. All 3D shapes are made * by connecting triangles to form their surfaces. By default, `detailY` is 1. * * The fifth parameter, `cap`, is also optional. If a `false` is passed, as * in `cone(20, 30, 5, 7, false)` the cone’s base won’t be drawn. By default, * `cap` is `true`. * * Note: `cone()` can only be used in WebGL mode. * * @method cone * @param {Number} [radius] radius of the cone's base. Defaults to 50. * @param {Number} [height] height of the cone. Defaults to the value of `radius`. * @param {Integer} [detailX] number of edges used to draw the base. Defaults to 24. * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 1. * @param {Boolean} [cap] whether to draw the cone's base. Defaults to `true`. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cone on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cone. * cone(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cone on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cone. * // Set its radius and height to 30. * cone(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cone on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cone. * // Set its radius to 30 and height to 50. * cone(30, 50); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cone on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cone. * // Set its radius to 30 and height to 50. * // Set its detailX to 5. * cone(30, 50, 5); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white pyramid on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cone. * // Set its radius to 30 and height to 50. * // Set its detailX to 5. * cone(30, 50, 5); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cone on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cone. * // Set its radius to 30 and height to 50. * // Set its detailX to 24 and detailY to 2. * cone(30, 50, 24, 2); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white cone on a gray background. Its base is missing.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the cone. * // Set its radius to 30 and height to 50. * // Set its detailX to 24 and detailY to 1. * // Don't draw its base. * cone(30, 50, 24, 1, false); * } */ fn.cone = function( radius = 50, height = radius, detailX = 24, detailY = 1, cap = true ) { this._assert3d('cone'); // p5._validateParameters('cone', arguments); this._renderer.cone(radius, height, detailX, detailY, cap); return this; }; /** * Draws an ellipsoid. * * An ellipsoid is a 3D shape with triangular faces that connect to form a * round surface. Ellipsoids with few faces look like crystals. Ellipsoids * with many faces have smooth surfaces and look like eggs. `ellipsoid()` * defines a shape by its radii. This is different from * ellipse() which uses diameters * (width and height). * * The first parameter, `radiusX`, is optional. If a `Number` is passed, as in * `ellipsoid(20)`, it sets the radius of the ellipsoid along the x-axis. By * default, `radiusX` is 50. * * The second parameter, `radiusY`, is also optional. If a `Number` is passed, * as in `ellipsoid(20, 30)`, it sets the ellipsoid’s radius along the y-axis. * By default, `radiusY` is set to the ellipsoid’s `radiusX`. * * The third parameter, `radiusZ`, is also optional. If a `Number` is passed, * as in `ellipsoid(20, 30, 40)`, it sets the ellipsoid’s radius along the * z-axis. By default, `radiusZ` is set to the ellipsoid’s `radiusY`. * * The fourth parameter, `detailX`, is also optional. If a `Number` is passed, * as in `ellipsoid(20, 30, 40, 5)`, it sets the number of triangle * subdivisions to use along the x-axis. All 3D shapes are made by connecting * triangles to form their surfaces. By default, `detailX` is 24. * * The fifth parameter, `detailY`, is also optional. If a `Number` is passed, * as in `ellipsoid(20, 30, 40, 5, 7)`, it sets the number of triangle * subdivisions to use along the y-axis. All 3D shapes are made by connecting * triangles to form their surfaces. By default, `detailY` is 16. * * Note: `ellipsoid()` can only be used in WebGL mode. * * @method ellipsoid * @param {Number} [radiusX] radius of the ellipsoid along the x-axis. Defaults to 50. * @param {Number} [radiusY] radius of the ellipsoid along the y-axis. Defaults to `radiusX`. * @param {Number} [radiusZ] radius of the ellipsoid along the z-axis. Defaults to `radiusY`. * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24. * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the ellipsoid. * // Set its radiusX to 30. * ellipsoid(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white ellipsoid on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the ellipsoid. * // Set its radiusX to 30. * // Set its radiusY to 40. * ellipsoid(30, 40); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white ellipsoid on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the ellipsoid. * // Set its radiusX to 30. * // Set its radiusY to 40. * // Set its radiusZ to 50. * ellipsoid(30, 40, 50); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white ellipsoid on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the ellipsoid. * // Set its radiusX to 30. * // Set its radiusY to 40. * // Set its radiusZ to 50. * // Set its detailX to 4. * ellipsoid(30, 40, 50, 4); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white ellipsoid on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the ellipsoid. * // Set its radiusX to 30. * // Set its radiusY to 40. * // Set its radiusZ to 50. * // Set its detailX to 4. * // Set its detailY to 3. * ellipsoid(30, 40, 50, 4, 3); * } */ fn.ellipsoid = function( radiusX = 50, radiusY = radiusX, radiusZ = radiusX, detailX = 24, detailY = 16 ) { this._assert3d('ellipsoid'); // p5._validateParameters('ellipsoid', arguments); this._renderer.ellipsoid(radiusX, radiusY, radiusZ, detailX, detailY); return this; }; /** * Draws a torus. * * A torus is a 3D shape with triangular faces that connect to form a ring. * Toruses with few faces look flattened. Toruses with many faces have smooth * surfaces. * * The first parameter, `radius`, is optional. If a `Number` is passed, as in * `torus(30)`, it sets the radius of the ring. By default, `radius` is 50. * * The second parameter, `tubeRadius`, is also optional. If a `Number` is * passed, as in `torus(30, 15)`, it sets the radius of the tube. By default, * `tubeRadius` is 10. * * The third parameter, `detailX`, is also optional. If a `Number` is passed, * as in `torus(30, 15, 5)`, it sets the number of edges used to draw the hole * of the torus. Using more edges makes the hole look more like a circle. By * default, `detailX` is 24. * * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, * as in `torus(30, 15, 5, 7)`, it sets the number of triangle subdivisions to * use while filling in the torus’ height. By default, `detailY` is 16. * * Note: `torus()` can only be used in WebGL mode. * * @method torus * @param {Number} [radius] radius of the torus. Defaults to 50. * @param {Number} [tubeRadius] radius of the tube. Defaults to 10. * @param {Integer} [detailX] number of edges that form the hole. Defaults to 24. * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white torus on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the torus. * torus(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white torus on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the torus. * // Set its radius to 30. * torus(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white torus on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the torus. * // Set its radius to 30 and tubeRadius to 15. * torus(30, 15); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white torus on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the torus. * // Set its radius to 30 and tubeRadius to 15. * // Set its detailX to 5. * torus(30, 15, 5); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white torus on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the torus. * // Set its radius to 30 and tubeRadius to 15. * // Set its detailX to 5. * // Set its detailY to 3. * torus(30, 15, 5, 3); * } */ fn.torus = function(radius, tubeRadius, detailX, detailY) { this._assert3d('torus'); // p5._validateParameters('torus', arguments); this._renderer.torus(radius, tubeRadius, detailX, detailY); return this; }; /////////////////////// /// 2D primitives /// /////////////////////// // // Note: Documentation is not generated on the p5.js website for functions on // the p5.Renderer3D prototype. /** * Draws a point, a coordinate in space at the dimension of one pixel, * given x, y and z coordinates. The color of the point is determined * by the current stroke, while the point size is determined by current * stroke weight. * @private * @param {Number} x x-coordinate of point * @param {Number} y y-coordinate of point * @param {Number} z z-coordinate of point * @chainable * @example * function setup() { * createCanvas(100, 100, WEBGL); * } * * function draw() { * background(50); * stroke(255); * strokeWeight(4); * point(25, 0); * strokeWeight(3); * point(-25, 0); * strokeWeight(2); * point(0, 25); * strokeWeight(1); * point(0, -25); * } */ Renderer3D.prototype.point = function(x, y, z = 0) { this.beginShape(POINTS); this.vertex(x, y, z); this.endShape(); return this; }; Renderer3D.prototype.triangle = function(args) { const x1 = args[0], y1 = args[1]; const x2 = args[2], y2 = args[3]; const x3 = args[4], y3 = args[5]; const gid = 'tri'; if (!this.geometryInHash(gid)) { const _triangle = function() { const vertices = []; vertices.push(new Vector(0, 0, 0)); vertices.push(new Vector(1, 0, 0)); vertices.push(new Vector(0, 1, 0)); this.edges = [[0, 1], [1, 2], [2, 0]]; this.vertices = vertices; this.faces = [[0, 1, 2]]; this.uvs = [0, 0, 1, 0, 1, 1]; }; const triGeom = new Geometry(1, 1, _triangle, this); triGeom._edgesToVertices(); triGeom.computeNormals(); triGeom.gid = gid; this.geometryBufferCache.ensureCached(triGeom); } // only one triangle is cached, one point is at the origin, and the // two adjacent sides are tne unit vectors along the X & Y axes. // // this matrix multiplication transforms those two unit vectors // onto the required vector prior to rendering, and moves the // origin appropriately. const uModelMatrix = this.states.uModelMatrix.copy(); try { // triangle orientation. const orientation = Math.sign(x1*y2-x2*y1 + x2*y3-x3*y2 + x3*y1-x1*y3); const mult = new Matrix([ x2 - x1, y2 - y1, 0, 0, // the resulting unit X-axis x3 - x1, y3 - y1, 0, 0, // the resulting unit Y-axis 0, 0, orientation, 0, // the resulting unit Z-axis (Reflect the specified order of vertices) x1, y1, 0, 1 // the resulting origin ]).mult(this.states.uModelMatrix); this.states.setValue('uModelMatrix', mult); this.model(this.geometryBufferCache.getGeometryByID(gid)); } finally { this.states.setValue('uModelMatrix', uModelMatrix); } return this; }; Renderer3D.prototype.ellipse = function(args) { this.arc( args[0], args[1], args[2], args[3], 0, TWO_PI, OPEN, args[4] ); }; Renderer3D.prototype.arc = function(...args) { const x = args[0]; const y = args[1]; const width = args[2]; const height = args[3]; const start = args[4]; const stop = args[5]; const mode = args[6]; const detail = args[7] || 25; let shape; let gid; // check if it is an ellipse or an arc if (Math.abs(stop - start) >= TWO_PI) { shape = 'ellipse'; gid = `${shape}|${detail}|`; } else { shape = 'arc'; gid = `${shape}|${start}|${stop}|${mode}|${detail}|`; } if (!this.geometryInHash(gid)) { const _arc = function() { // if the start and stop angles are not the same, push vertices to the array if (start.toFixed(10) !== stop.toFixed(10)) { // if the mode specified is PIE or null, push the mid point of the arc in vertices if (mode === PIE || typeof mode === 'undefined') { this.vertices.push(new Vector(0.5, 0.5, 0)); this.uvs.push([0.5, 0.5]); } // vertices for the perimeter of the circle for (let i = 0; i <= detail; i++) { const u = i / detail; const theta = (stop - start) * u + start; const _x = 0.5 + Math.cos(theta) / 2; const _y = 0.5 + Math.sin(theta) / 2; this.vertices.push(new Vector(_x, _y, 0)); this.uvs.push([_x, _y]); if (i < detail - 1) { this.faces.push([0, i + 1, i + 2]); this.edges.push([i + 1, i + 2]); } } // check the mode specified in order to push vertices and faces, different for each mode switch (mode) { case PIE: this.faces.push([ 0, this.vertices.length - 2, this.vertices.length - 1 ]); this.edges.push([0, 1]); this.edges.push([ this.vertices.length - 2, this.vertices.length - 1 ]); this.edges.push([0, this.vertices.length - 1]); break; case CHORD: this.edges.push([0, 1]); this.edges.push([0, this.vertices.length - 1]); break; case OPEN: this.edges.push([0, 1]); break; default: this.faces.push([ 0, this.vertices.length - 2, this.vertices.length - 1 ]); this.edges.push([ this.vertices.length - 2, this.vertices.length - 1 ]); } } }; const arcGeom = new Geometry(detail, 1, _arc, this); arcGeom.computeNormals(); if (detail <= 50) { arcGeom._edgesToVertices(arcGeom); } else if (this.states.strokeColor) { console.log( `Cannot apply a stroke to an ${shape} with more than 50 detail` ); } arcGeom.gid = gid; this.geometryBufferCache.ensureCached(arcGeom); } const uModelMatrix = this.states.uModelMatrix; this.states.setValue('uModelMatrix', this.states.uModelMatrix.clone()); try { this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); this.model(this.geometryBufferCache.getGeometryByID(gid)); } finally { this.states.setValue('uModelMatrix', uModelMatrix); } return this; }; Renderer3D.prototype.rect = function(args) { const x = args[0]; const y = args[1]; const width = args[2]; const height = args[3]; 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 ?? true; 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 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.edges = [ [0, detailX], [detailX, (detailX + 1) * (detailY + 1) - 1], [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY], [(detailX + 1) * detailY, 0] ]; } }; const rectGeom = new Geometry(detailX, detailY, _rect, this); rectGeom .computeFaces() .computeNormals() ._edgesToVertices(); rectGeom.gid = gid; this.geometryBufferCache.ensureCached(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 uModelMatrix = this.states.uModelMatrix; this.states.setValue('uModelMatrix', this.states.uModelMatrix.copy()); try { this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); this.model(this.geometryBufferCache.getGeometryByID(gid)); } finally { this.states.setValue('uModelMatrix', uModelMatrix); } } 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; } 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; const prevMode = this.states.textureMode; this.states.setValue('textureMode', NORMAL); const prevOrder = this.bezierOrder(); this.bezierOrder(2); this.beginShape(); const addUVs = (x, y) => [x, y, 0, (x - x1)/width, (y - y1)/height]; if (tr !== 0) { this.vertex(...addUVs(x2 - tr, y1)); this.bezierVertex(...addUVs(x2, y1)); this.bezierVertex(...addUVs(x2, y1 + tr)); } else { this.vertex(...addUVs(x2, y1)); } if (br !== 0) { this.vertex(...addUVs(x2, y2 - br)); this.bezierVertex(...addUVs(x2, y2)); this.bezierVertex(...addUVs(x2 - br, y2)); } else { this.vertex(...addUVs(x2, y2)); } if (bl !== 0) { this.vertex(...addUVs(x1 + bl, y2)); this.bezierVertex(...addUVs(x1, y2)); this.bezierVertex(...addUVs(x1, y2 - bl)); } else { this.vertex(...addUVs(x1, y2)); } if (tl !== 0) { this.vertex(...addUVs(x1, y1 + tl)); this.bezierVertex(...addUVs(x1, y1)); this.bezierVertex(...addUVs(x1 + tl, y1)); } else { this.vertex(...addUVs(x1, y1)); } this.endShape(CLOSE); this.states.setValue('textureMode', prevMode); this.bezierOrder(prevOrder); } return this; }; Renderer3D.prototype.quad = function( x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, detailX=2, detailY=2 ) { const gid = `quad|${x1}|${y1}|${z1}|${x2}|${y2}|${z2}|${x3}|${y3}|${z3}|${x4}|${y4}|${z4}|${detailX}|${detailY}`; if (!this.geometryInHash(gid)) { const quadGeom = new Geometry(detailX, detailY, function() { //algorithm adapted from c++ to js //https://stackoverflow.com/questions/16989181/whats-the-correct-way-to-draw-a-distorted-plane-in-opengl/16993202#16993202 let xRes = 1.0 / (this.detailX - 1); let yRes = 1.0 / (this.detailY - 1); for (let y = 0; y < this.detailY; y++) { for (let x = 0; x < this.detailX; x++) { let pctx = x * xRes; let pcty = y * yRes; let linePt0x = (1 - pcty) * x1 + pcty * x4; let linePt0y = (1 - pcty) * y1 + pcty * y4; let linePt0z = (1 - pcty) * z1 + pcty * z4; let linePt1x = (1 - pcty) * x2 + pcty * x3; let linePt1y = (1 - pcty) * y2 + pcty * y3; let linePt1z = (1 - pcty) * z2 + pcty * z3; let ptx = (1 - pctx) * linePt0x + pctx * linePt1x; let pty = (1 - pctx) * linePt0y + pctx * linePt1y; let ptz = (1 - pctx) * linePt0z + pctx * linePt1z; this.vertices.push(new Vector(ptx, pty, ptz)); this.uvs.push([pctx, pcty]); } } }, this); quadGeom.faces = []; for(let y = 0; y < detailY-1; y++){ for(let x = 0; x < detailX-1; x++){ let pt0 = x + y * detailX; let pt1 = (x + 1) + y * detailX; let pt2 = (x + 1) + (y + 1) * detailX; let pt3 = x + (y + 1) * detailX; quadGeom.faces.push([pt0, pt1, pt2]); quadGeom.faces.push([pt0, pt2, pt3]); } } quadGeom.computeNormals(); quadGeom.edges.length = 0; const vertexOrder = [0, 2, 3, 1]; for (let i = 0; i < vertexOrder.length; i++) { const startVertex = vertexOrder[i]; const endVertex = vertexOrder[(i + 1) % vertexOrder.length]; quadGeom.edges.push([startVertex, endVertex]); } quadGeom._edgesToVertices(); quadGeom.gid = gid; this.geometryBufferCache.ensureCached(quadGeom); } this.model(this.geometryBufferCache.getGeometryByID(gid)); return this; }; //this implementation of bezier curve //is based on Bernstein polynomial // pretier-ignore Renderer3D.prototype.bezier = function( x1, y1, z1, // x2 x2, // y2 y2, // x3 z2, // y3 x3, // x4 y3, // y4 z3, x4, y4, z4 ) { if (arguments.length === 8) { y4 = y3; x4 = x3; y3 = z2; x3 = y2; y2 = x2; x2 = z1; z1 = z2 = z3 = z4 = 0; } // TODO: handle quadratic? this.bezierOrder(); this.bezierOrder(3); this.beginShape(); this.vertex(x1, y1, z1); this.bezierVertex(x2, y2, z2); this.bezierVertex(x3, y3, z3); this.bezierVertex(x4, y4, z4); this.endShape(); }; // pretier-ignore Renderer3D.prototype.curve = function( x1, y1, z1, // x2 x2, // y2 y2, // x3 z2, // y3 x3, // x4 y3, // y4 z3, x4, y4, z4 ) { if (arguments.length === 8) { x4 = x3; y4 = y3; x3 = y2; y3 = x2; x2 = z1; y2 = x2; z1 = z2 = z3 = z4 = 0; } this.beginShape(); this.splineVertex(x1, y1, z1); this.splineVertex(x2, y2, z2); this.splineVertex(x3, y3, z3); this.splineVertex(x4, y4, z4); this.endShape(); }; /** * Draw a line given two points * @private * @param {Number} x0 x-coordinate of first vertex * @param {Number} y0 y-coordinate of first vertex * @param {Number} z0 z-coordinate of first vertex * @param {Number} x1 x-coordinate of second vertex * @param {Number} y1 y-coordinate of second vertex * @param {Number} z1 z-coordinate of second vertex * @chainable * @example * //draw a line * function setup() { * createCanvas(100, 100, WEBGL); * } * * function draw() { * background(200); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * // Use fill instead of stroke to change the color of shape. * fill(255, 0, 0); * line(10, 10, 0, 60, 60, 20); * } */ Renderer3D.prototype.line = function(...args) { if (args.length === 6) { // TODO shapes refactor this.beginShape(LINES); this.vertex(args[0], args[1], args[2]); this.vertex(args[3], args[4], args[5]); this.endShape(); } else if (args.length === 4) { this.beginShape(LINES); this.vertex(args[0], args[1], 0); this.vertex(args[2], args[3], 0); this.endShape(); } return this; }; Renderer3D.prototype.image = function( img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight ) { // console.log(arguments); if (this._isErasing) { this.blendMode(this._cachedBlendMode); } this.push(); this.noLights(); this.states.setValue('strokeColor', null); this.texture(img); this.states.setValue('textureMode', NORMAL); let u0 = 0; if (sx <= img.width) { u0 = sx / img.width; } let u1 = 1; if (sx + sWidth <= img.width) { u1 = (sx + sWidth) / img.width; } let v0 = 0; if (sy <= img.height) { v0 = sy / img.height; } let v1 = 1; if (sy + sHeight <= img.height) { v1 = (sy + sHeight) / img.height; } this._drawingImage = true; this.beginShape(); this.vertex(dx, dy, 0, u0, v0); this.vertex(dx + dWidth, dy, 0, u1, v0); this.vertex(dx + dWidth, dy + dHeight, 0, u1, v1); this.vertex(dx, dy + dHeight, 0, u0, v1); this.endShape(CLOSE); this._drawingImage = false; this.pop(); if (this._isErasing) { this.blendMode(REMOVE); } }; /////////////////////// /// 3D primitives /// /////////////////////// /** * @private * Helper function for creating both cones and cylinders * Will only generate well-defined geometry when bottomRadius, height > 0 * and topRadius >= 0 * If topRadius == 0, topCap should be false */ const _truncatedCone = function( bottomRadius, topRadius, height, detailX, detailY, bottomCap, topCap ) { bottomRadius = bottomRadius <= 0 ? 1 : bottomRadius; topRadius = topRadius < 0 ? 0 : topRadius; height = height <= 0 ? bottomRadius : height; detailX = detailX < 3 ? 3 : detailX; detailY = detailY < 1 ? 1 : detailY; bottomCap = bottomCap === undefined ? true : bottomCap; topCap = topCap === undefined ? topRadius !== 0 : topCap; const start = bottomCap ? -2 : 0; const end = detailY + (topCap ? 2 : 0); //ensure constant slant for interior vertex normals const slant = Math.atan2(bottomRadius - topRadius, height); const sinSlant = Math.sin(slant); const cosSlant = Math.cos(slant); let yy, ii, jj; for (yy = start; yy <= end; ++yy) { let v = yy / detailY; let y = height * v; let ringRadius; if (yy < 0) { //for the bottomCap edge y = 0; v = 0; ringRadius = bottomRadius; } else if (yy > detailY) { //for the topCap edge y = height; v = 1; ringRadius = topRadius; } else { //for the middle ringRadius = bottomRadius + (topRadius - bottomRadius) * v; } if (yy === -2 || yy === detailY + 2) { //center of bottom or top caps ringRadius = 0; } y -= height / 2; //shift coordiate origin to the center of object for (ii = 0; ii < detailX; ++ii) { const u = ii / (detailX - 1); const ur = 2 * Math.PI * u; const sur = Math.sin(ur); const cur = Math.cos(ur); //VERTICES this.vertices.push(new Vector(sur * ringRadius, y, cur * ringRadius)); //VERTEX NORMALS let vertexNormal; if (yy < 0) { vertexNormal = new Vector(0, -1, 0); } else if (yy > detailY && topRadius) { vertexNormal = new Vector(0, 1, 0); } else { vertexNormal = new Vector(sur * cosSlant, sinSlant, cur * cosSlant); } this.vertexNormals.push(vertexNormal); //UVs this.uvs.push(u, v); } } let startIndex = 0; if (bottomCap) { for (jj = 0; jj < detailX; ++jj) { const nextjj = (jj + 1) % detailX; this.faces.push([ startIndex + jj, startIndex + detailX + nextjj, startIndex + detailX + jj ]); } startIndex += detailX * 2; } for (yy = 0; yy < detailY; ++yy) { for (ii = 0; ii < detailX; ++ii) { const nextii = (ii + 1) % detailX; this.faces.push([ startIndex + ii, startIndex + nextii, startIndex + detailX + nextii ]); this.faces.push([ startIndex + ii, startIndex + detailX + nextii, startIndex + detailX + ii ]); } startIndex += detailX; } if (topCap) { startIndex += detailX; for (ii = 0; ii < detailX; ++ii) { this.faces.push([ startIndex + ii, startIndex + (ii + 1) % detailX, startIndex + detailX ]); } } }; Renderer3D.prototype.plane = function( width = 50, height = width, detailX = 1, detailY = 1 ) { const gid = `plane|${detailX}|${detailY}`; if (!this.geometryInHash(gid)) { const _plane = function() { let u, v, p; for (let i = 0; i <= this.detailY; i++) { v = i / this.detailY; for (let j = 0; j <= this.detailX; j++) { u = j / this.detailX; p = new Vector(u - 0.5, v - 0.5, 0); this.vertices.push(p); this.uvs.push(u, v); } } }; const planeGeom = new Geometry(detailX, detailY, _plane, this); planeGeom.computeFaces().computeNormals(); if (detailX <= 1 && detailY <= 1) { planeGeom._makeTriangleEdges()._edgesToVertices(); } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on plane objects with more' + ' than 1 detailX or 1 detailY' ); } planeGeom.gid = gid; this.geometryBufferCache.ensureCached(planeGeom); } this._drawGeometryScaled( this.geometryBufferCache.getGeometryByID(gid), width, height, 1 ); }; Renderer3D.prototype.box = function( width = 50, height = width, depth = height, detailX, detailY ){ const perPixelLighting = this.attributes && this.attributes.perPixelLighting; if (typeof detailX === 'undefined') { detailX = perPixelLighting ? 1 : 4; } if (typeof detailY === 'undefined') { detailY = perPixelLighting ? 1 : 4; } const gid = `box|${detailX}|${detailY}`; if (!this.geometryInHash(gid)) { const _box = function() { const cubeIndices = [ [0, 4, 2, 6], // -1, 0, 0],// -x [1, 3, 5, 7], // +1, 0, 0],// +x [0, 1, 4, 5], // 0, -1, 0],// -y [2, 6, 3, 7], // 0, +1, 0],// +y [0, 2, 1, 3], // 0, 0, -1],// -z [4, 5, 6, 7] // 0, 0, +1] // +z ]; //using custom edges //to avoid diagonal stroke lines across face of box this.edges = [ [0, 1], [1, 3], [3, 2], [6, 7], [8, 9], [9, 11], [14, 15], [16, 17], [17, 19], [18, 19], [20, 21], [22, 23] ]; cubeIndices.forEach((cubeIndex, i) => { const v = i * 4; for (let j = 0; j < 4; j++) { const d = cubeIndex[j]; //inspired by lightgl: //https://github.com/evanw/lightgl.js //octants:https://en.wikipedia.org/wiki/Octant_(solid_geometry) const octant = new Vector( ((d & 1) * 2 - 1) / 2, ((d & 2) - 1) / 2, ((d & 4) / 2 - 1) / 2 ); this.vertices.push(octant); this.uvs.push(j & 1, (j & 2) / 2); } this.faces.push([v, v + 1, v + 2]); this.faces.push([v + 2, v + 1, v + 3]); }); }; const boxGeom = new Geometry(detailX, detailY, _box, this); boxGeom.computeNormals(); if (detailX <= 4 && detailY <= 4) { boxGeom._edgesToVertices(); } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on box objects with more' + ' than 4 detailX or 4 detailY' ); } //initialize our geometry buffer with //the key val pair: //geometry Id, Geom object boxGeom.gid = gid; this.geometryBufferCache.ensureCached(boxGeom); } this._drawGeometryScaled( this.geometryBufferCache.getGeometryByID(gid), width, height, depth ); }; Renderer3D.prototype.sphere = function( radius = 50, detailX = 24, detailY = 16 ) { this.ellipsoid(radius, radius, radius, detailX, detailY); }; Renderer3D.prototype.ellipsoid = function( radiusX = 50, radiusY = radiusX, radiusZ = radiusX, detailX = 24, detailY = 16 ) { const gid = `ellipsoid|${detailX}|${detailY}`; if (!this.geometryInHash(gid)) { const _ellipsoid = function() { for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; const phi = Math.PI * v - Math.PI / 2; const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); for (let j = 0; j <= this.detailX; j++) { const u = j / this.detailX; const theta = 2 * Math.PI * u; const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); const p = new p5.Vector( cosPhi * sinTheta, sinPhi, cosPhi * cosTheta ); this.vertices.push(p); this.vertexNormals.push(p); this.uvs.push(u, v); } } }; const ellipsoidGeom = new Geometry(detailX, detailY, _ellipsoid, this); ellipsoidGeom.computeFaces(); if (detailX <= 24 && detailY <= 24) { ellipsoidGeom._makeTriangleEdges()._edgesToVertices(); } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on ellipsoids with more' + ' than 24 detailX or 24 detailY' ); } ellipsoidGeom.gid = gid; this.geometryBufferCache.ensureCached(ellipsoidGeom); } this._drawGeometryScaled( this.geometryBufferCache.getGeometryByID(gid), radiusX, radiusY, radiusZ ); }; Renderer3D.prototype.cylinder = function( radius = 50, height = radius, detailX = 24, detailY = 1, bottomCap = true, topCap = true ) { const gid = `cylinder|${detailX}|${detailY}|${bottomCap}|${topCap}`; if (!this.geometryInHash(gid)) { const cylinderGeom = new p5.Geometry(detailX, detailY, function() { _truncatedCone.call( this, 1, 1, 1, detailX, detailY, bottomCap, topCap ); }, this); // normals are computed in call to _truncatedCone if (detailX <= 24 && detailY <= 16) { cylinderGeom._makeTriangleEdges()._edgesToVertices(); } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on cylinder objects with more' + ' than 24 detailX or 16 detailY' ); } cylinderGeom.gid = gid; this.geometryBufferCache.ensureCached(cylinderGeom); } this._drawGeometryScaled( this.geometryBufferCache.getGeometryByID(gid), radius, height, radius ); }; Renderer3D.prototype.cone = function( radius = 50, height = radius, detailX = 24, detailY = 1, cap = true ) { const gid = `cone|${detailX}|${detailY}|${cap}`; if (!this.geometryInHash(gid)) { const coneGeom = new Geometry(detailX, detailY, function() { _truncatedCone.call( this, 1, 0, 1, detailX, detailY, cap, false ); }, this); if (detailX <= 24 && detailY <= 16) { coneGeom._makeTriangleEdges()._edgesToVertices(); } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on cone objects with more' + ' than 24 detailX or 16 detailY' ); } coneGeom.gid = gid; this.geometryBufferCache.ensureCached(coneGeom); } this._drawGeometryScaled( this.geometryBufferCache.getGeometryByID(gid), radius, height, radius ); }; Renderer3D.prototype.torus = function( radius = 50, tubeRadius = 10, detailX = 24, detailY = 16 ) { if (radius === 0) { return; // nothing to draw } if (tubeRadius === 0) { return; // nothing to draw } const tubeRatio = (tubeRadius / radius).toPrecision(4); const gid = `torus|${tubeRatio}|${detailX}|${detailY}`; if (!this.geometryInHash(gid)) { const _torus = function() { for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; const phi = 2 * Math.PI * v; const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); const r = 1 + tubeRatio * cosPhi; for (let j = 0; j <= this.detailX; j++) { const u = j / this.detailX; const theta = 2 * Math.PI * u; const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); const p = new Vector( r * cosTheta, r * sinTheta, tubeRatio * sinPhi ); const n = new Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); this.vertices.push(p); this.vertexNormals.push(n); this.uvs.push(u, v); } } }; const torusGeom = new Geometry(detailX, detailY, _torus, this); torusGeom.computeFaces(); if (detailX <= 24 && detailY <= 16) { torusGeom._makeTriangleEdges()._edgesToVertices(); } else if (this.states.strokeColor) { console.log( 'Cannot draw strokes on torus object with more' + ' than 24 detailX or 16 detailY' ); } torusGeom.gid = gid; this.geometryBufferCache.ensureCached(torusGeom); } this._drawGeometryScaled( this.geometryBufferCache.getGeometryByID(gid), radius, radius, radius ); }; /** * Sets the number of segments used to draw spline curves in WebGL mode. * * In WebGL mode, smooth shapes are drawn using many flat segments. Adding * more flat segments makes shapes appear smoother. * * The parameter, `detail`, is the density of segments to use while drawing a * spline curve. * * Note: `curveDetail()` has no effect in 2D mode. * * @method curveDetail * @param {Number} resolution number of segments to use. Default is 1/4 * @chainable * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Set the curveDetail() to 0.5 * curveDetail(0.5); * * // Do not show all the vertices * splineProperty('ends', EXCLUDE) * * // Draw a black spline curve. * noFill(); * strokeWeight(1); * stroke(0); * spline(-45, -24, 0, 23, -26, 0, 23, 11, 0, -35, 15, 0); * * // Draw red spline curves from the anchor points to the control points. * spline(255, 0, 0); * spline(-45, -24, 0, -45, -24, 0, 23, -26, 0, 23, 11, 0); * spline(23, -26, 0, 23, 11, 0, -35, 15, 0, -35, 15, 0); * * // Draw the anchor points in black. * strokeWeight(5); * stroke(0); * point(23, -26); * point(23, 11); * * // Draw the control points in red. * stroke(255, 0, 0); * point(-45, -24); * point(-35, 15); * * describe( * 'A gray square with a jagged curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.' * ); * } */ fn.curveDetail = function(d) { if (!(this._renderer instanceof Renderer3D)) { throw new Error( 'curveDetail() only works in WebGL mode. Did you mean to call createCanvas(width, height, WEBGL)?' ); } return this._renderer.curveDetail(d); }; } if(typeof p5 !== 'undefined'){ primitives3D(p5, p5.prototype); } /** * @module 3D * @submodule Lights * @for p5 * @requires core */ function light(p5, fn){ /** * Creates a light that shines from all directions. * * Ambient light does not come from one direction. Instead, 3D shapes are * lit evenly from all sides. Ambient lights are almost always used in * combination with other types of lights. * * There are three ways to call `ambientLight()` with optional parameters to * set the light’s color. * * The first way to call `ambientLight()` has two parameters, `gray` and * `alpha`. `alpha` is optional. Grayscale and alpha values between 0 and 255 * can be passed to set the ambient light’s color, as in `ambientLight(50)` or * `ambientLight(50, 30)`. * * The second way to call `ambientLight()` has one parameter, color. A * p5.Color object, an array of color values, or a * CSS color string, as in `ambientLight('magenta')`, can be passed to set the * ambient light’s color. * * The third way to call `ambientLight()` has four parameters, `v1`, `v2`, * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be * passed to set the ambient light’s colors, as in `ambientLight(255, 0, 0)` * or `ambientLight(255, 0, 0, 30)`. Color values will be interpreted using * the current colorMode(). * * @method ambientLight * @param {Number} v1 red or hue value in the current * colorMode(). * @param {Number} v2 green or saturation value in the current * colorMode(). * @param {Number} v3 blue, brightness, or lightness value in the current * colorMode(). * @param {Number} [alpha] alpha (transparency) value in the current * colorMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click the canvas to turn on the light. * * let isLit = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn against a gray background. The sphere appears to change color when the user double-clicks.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Control the light. * if (isLit === true) { * // Use a grayscale value of 80. * ambientLight(80); * } * * // Draw the sphere. * sphere(30); * } * * // Turn on the ambient light when the user double-clicks. * function doubleClicked() { * isLit = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A faded magenta sphere drawn against a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * // Use a p5.Color object. * let c = color('orchid'); * ambientLight(c); * * // Draw the sphere. * sphere(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A faded magenta sphere drawn against a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * // Use a CSS color string. * ambientLight('#DA70D6'); * * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A faded magenta sphere drawn against a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * // Use RGB values * ambientLight(218, 112, 214); * * // Draw the sphere. * sphere(30); * } */ /** * @method ambientLight * @param {Number} gray grayscale value between 0 and 255. * @param {Number} [alpha] * @chainable */ /** * @method ambientLight * @param {String} value color as a CSS string. * @chainable */ /** * @method ambientLight * @param {Number[]} values color as an array of RGBA, HSBA, or HSLA * values. * @chainable */ /** * @method ambientLight * @param {p5.Color} color color as a p5.Color object. * @chainable */ fn.ambientLight = function (v1, v2, v3, a) { this._assert3d('ambientLight'); // p5._validateParameters('ambientLight', arguments); this._renderer.ambientLight(...arguments); return this; }; /** * Sets the specular color for lights. * * `specularColor()` affects lights that bounce off a surface in a preferred * direction. These lights include * directionalLight(), * pointLight(), and * spotLight(). The function helps to create * highlights on p5.Geometry objects that are * styled with specularMaterial(). If a * geometry does not use * specularMaterial(), then * `specularColor()` will have no effect. * * Note: `specularColor()` doesn’t affect lights that bounce in all * directions, including ambientLight() and * imageLight(). * * There are three ways to call `specularColor()` with optional parameters to * set the specular highlight color. * * The first way to call `specularColor()` has two optional parameters, `gray` * and `alpha`. Grayscale and alpha values between 0 and 255, as in * `specularColor(50)` or `specularColor(50, 80)`, can be passed to set the * specular highlight color. * * The second way to call `specularColor()` has one optional parameter, * `color`. A p5.Color object, an array of color * values, or a CSS color string can be passed to set the specular highlight * color. * * The third way to call `specularColor()` has four optional parameters, `v1`, * `v2`, `v3`, and `alpha`. RGBA, HSBA, or HSLA values, as in * `specularColor(255, 0, 0, 80)`, can be passed to set the specular highlight * color. Color values will be interpreted using the current * colorMode(). * * @method specularColor * @param {Number} v1 red or hue value in the current * colorMode(). * @param {Number} v2 green or saturation value in the current * colorMode(). * @param {Number} v3 blue, brightness, or lightness value in the current * colorMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // No specular color. * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click the canvas to add a point light. * * let isLit = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn on a gray background. A spotlight starts shining when the user double-clicks.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Style the sphere. * noStroke(); * specularColor(100); * specularMaterial(255, 255, 255); * * // Control the light. * if (isLit === true) { * // Add a white point light from the top-right. * pointLight(255, 255, 255, 30, -20, 40); * } * * // Draw the sphere. * sphere(30); * } * * // Turn on the point light when the user double-clicks. * function doubleClicked() { * isLit = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a specular highlight. * // Use a p5.Color object. * let c = color('dodgerblue'); * specularColor(c); * * // Add a white point light from the top-right. * pointLight(255, 255, 255, 30, -20, 40); * * // Style the sphere. * noStroke(); * * // Add a white specular material. * specularMaterial(255, 255, 255); * * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a specular highlight. * // Use a CSS color string. * specularColor('#1E90FF'); * * // Add a white point light from the top-right. * pointLight(255, 255, 255, 30, -20, 40); * * // Style the sphere. * noStroke(); * * // Add a white specular material. * specularMaterial(255, 255, 255); * * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a specular highlight. * // Use RGB values. * specularColor(30, 144, 255); * * // Add a white point light from the top-right. * pointLight(255, 255, 255, 30, -20, 40); * * // Style the sphere. * noStroke(); * * // Add a white specular material. * specularMaterial(255, 255, 255); * * // Draw the sphere. * sphere(30); * } */ /** * @method specularColor * @param {Number} gray grayscale value between 0 and 255. * @chainable */ /** * @method specularColor * @param {String} value color as a CSS string. * @chainable */ /** * @method specularColor * @param {Number[]} values color as an array of RGBA, HSBA, or HSLA * values. * @chainable */ /** * @method specularColor * @param {p5.Color} color color as a p5.Color object. * @chainable */ fn.specularColor = function (v1, v2, v3) { this._assert3d('specularColor'); // p5._validateParameters('specularColor', arguments); this._renderer.specularColor(...arguments); return this; }; /** * Creates a light that shines in one direction. * * Directional lights don’t shine from a specific point. They’re like a sun * that shines from somewhere offscreen. The light’s direction is set using * three `(x, y, z)` values between -1 and 1. For example, setting a light’s * direction as `(1, 0, 0)` will light p5.Geometry * objects from the left since the light faces directly to the right. A * maximum of 5 directional lights can be active at once. * * There are four ways to call `directionalLight()` with parameters to set the * light’s color and direction. * * The first way to call `directionalLight()` has six parameters. The first * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the * current colorMode(). The last three * parameters, `x`, `y`, and `z`, set the light’s direction. For example, * `directionalLight(255, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light * that shines to the right `(1, 0, 0)`. * * The second way to call `directionalLight()` has four parameters. The first * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the * current colorMode(). The last parameter, * `direction` sets the light’s direction using a * p5.Vector object. For example, * `directionalLight(255, 0, 0, lightDir)` creates a red `(255, 0, 0)` light * that shines in the direction the `lightDir` vector points. * * The third way to call `directionalLight()` has four parameters. The first * parameter, `color`, sets the light’s color using a * p5.Color object or an array of color values. The * last three parameters, `x`, `y`, and `z`, set the light’s direction. For * example, `directionalLight(myColor, 1, 0, 0)` creates a light that shines * to the right `(1, 0, 0)` with the color value of `myColor`. * * The fourth way to call `directionalLight()` has two parameters. The first * parameter, `color`, sets the light’s color using a * p5.Color object or an array of color values. The * second parameter, `direction`, sets the light’s direction using a * p5.Vector object. For example, * `directionalLight(myColor, lightDir)` creates a light that shines in the * direction the `lightDir` vector points with the color value of `myColor`. * * @method directionalLight * @param {Number} v1 red or hue value in the current * colorMode(). * @param {Number} v2 green or saturation value in the current * colorMode(). * @param {Number} v3 blue, brightness, or lightness value in the current * colorMode(). * @param {Number} x x-component of the light's direction between -1 and 1. * @param {Number} y y-component of the light's direction between -1 and 1. * @param {Number} z z-component of the light's direction between -1 and 1. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click to turn on the directional light. * * let isLit = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Control the light. * if (isLit === true) { * // Add a red directional light from above. * // Use RGB values and XYZ directions. * directionalLight(255, 0, 0, 0, 1, 0); * } * * // Style the sphere. * noStroke(); * * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a red directional light from above. * // Use a p5.Color object and XYZ directions. * let c = color(255, 0, 0); * directionalLight(c, 0, 1, 0); * * // Style the sphere. * noStroke(); * * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a red directional light from above. * // Use a p5.Color object and a p5.Vector object. * let c = color(255, 0, 0); * let lightDir = createVector(0, 1, 0); * directionalLight(c, lightDir); * * // Style the sphere. * noStroke(); * * // Draw the sphere. * sphere(30); * } */ /** * @method directionalLight * @param {Number} v1 * @param {Number} v2 * @param {Number} v3 * @param {p5.Vector} direction direction of the light as a * p5.Vector object. * @chainable */ /** * @method directionalLight * @param {p5.Color|Number[]|String} color color as a p5.Color object, * an array of color values, or as a CSS string. * @param {Number} x * @param {Number} y * @param {Number} z * @chainable */ /** * @method directionalLight * @param {p5.Color|Number[]|String} color * @param {p5.Vector} direction * @chainable */ fn.directionalLight = function (v1, v2, v3, x, y, z) { this._assert3d('directionalLight'); // p5._validateParameters('directionalLight', arguments); //@TODO: check parameters number this._renderer.directionalLight(...arguments); return this; }; /** * Creates a light that shines from a point in all directions. * * Point lights are like light bulbs that shine in all directions. They can be * placed at different positions to achieve different lighting effects. A * maximum of 5 point lights can be active at once. * * There are four ways to call `pointLight()` with parameters to set the * light’s color and position. * * The first way to call `pointLight()` has six parameters. The first three * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current * colorMode(). The last three parameters, `x`, * `y`, and `z`, set the light’s position. For example, * `pointLight(255, 0, 0, 50, 0, 0)` creates a red `(255, 0, 0)` light that * shines from the coordinates `(50, 0, 0)`. * * The second way to call `pointLight()` has four parameters. The first three * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current * colorMode(). The last parameter, position sets * the light’s position using a p5.Vector object. * For example, `pointLight(255, 0, 0, lightPos)` creates a red `(255, 0, 0)` * light that shines from the position set by the `lightPos` vector. * * The third way to call `pointLight()` has four parameters. The first * parameter, `color`, sets the light’s color using a * p5.Color object or an array of color values. The * last three parameters, `x`, `y`, and `z`, set the light’s position. For * example, `directionalLight(myColor, 50, 0, 0)` creates a light that shines * from the coordinates `(50, 0, 0)` with the color value of `myColor`. * * The fourth way to call `pointLight()` has two parameters. The first * parameter, `color`, sets the light’s color using a * p5.Color object or an array of color values. The * second parameter, `position`, sets the light’s position using a * p5.Vector object. For example, * `directionalLight(myColor, lightPos)` creates a light that shines from the * position set by the `lightPos` vector with the color value of `myColor`. * * @method pointLight * @param {Number} v1 red or hue value in the current * colorMode(). * @param {Number} v2 green or saturation value in the current * colorMode(). * @param {Number} v3 blue, brightness, or lightness value in the current * colorMode(). * @param {Number} x x-coordinate of the light. * @param {Number} y y-coordinate of the light. * @param {Number} z z-coordinate of the light. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click to turn on the point light. * * let isLit = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Control the light. * if (isLit === true) { * // Add a red point light from above. * // Use RGB values and XYZ coordinates. * pointLight(255, 0, 0, 0, -150, 0); * } * * // Style the sphere. * noStroke(); * * // Draw the sphere. * sphere(30); * } * * // Turn on the point light when the user double-clicks. * function doubleClicked() { * isLit = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a red point light from above. * // Use a p5.Color object and XYZ directions. * let c = color(255, 0, 0); * pointLight(c, 0, -150, 0); * * // Style the sphere. * noStroke(); * * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a red point light from above. * // Use a p5.Color object and a p5.Vector object. * let c = color(255, 0, 0); * let lightPos = createVector(0, -150, 0); * pointLight(c, lightPos); * * // Style the sphere. * noStroke(); * * // Draw the sphere. * sphere(30); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('Four spheres arranged in a square and drawn on a gray background. The spheres appear bright red toward the center of the square. The color gets darker toward the corners of the square.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Add a red point light that points to the center of the scene. * // Use a p5.Color object and a p5.Vector object. * let c = color(255, 0, 0); * let lightPos = createVector(0, 0, 65); * pointLight(c, lightPos); * * // Style the spheres. * noStroke(); * * // Draw a sphere up and to the left. * push(); * translate(-25, -25, 25); * sphere(10); * pop(); * * // Draw a box up and to the right. * push(); * translate(25, -25, 25); * sphere(10); * pop(); * * // Draw a sphere down and to the left. * push(); * translate(-25, 25, 25); * sphere(10); * pop(); * * // Draw a box down and to the right. * push(); * translate(25, 25, 25); * sphere(10); * pop(); * } */ /** * @method pointLight * @param {Number} v1 * @param {Number} v2 * @param {Number} v3 * @param {p5.Vector} position position of the light as a * p5.Vector object. * @chainable */ /** * @method pointLight * @param {p5.Color|Number[]|String} color color as a p5.Color object, * an array of color values, or a CSS string. * @param {Number} x * @param {Number} y * @param {Number} z * @chainable */ /** * @method pointLight * @param {p5.Color|Number[]|String} color * @param {p5.Vector} position * @chainable */ fn.pointLight = function (v1, v2, v3, x, y, z) { this._assert3d('pointLight'); // p5._validateParameters('pointLight', arguments); //@TODO: check parameters number this._renderer.pointLight(...arguments); return this; }; /** * Creates an ambient light from an image. * * `imageLight()` simulates a light shining from all directions. The effect is * like placing the sketch at the center of a giant sphere that uses the image * as its texture. The image's diffuse light will be affected by * fill() and the specular reflections will be * affected by specularMaterial() and * shininess(). * * The parameter, `img`, is the p5.Image object to * use as the light source. * * @method imageLight * @param {p5.Image} img image to use as the light source. * * @example * // Click and drag the mouse to view the scene from different angles. * * let img; * * async function setup() { * // Load an image and create a p5.Image object. * img = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(100, 100, WEBGL); * * describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape.'); * } * * function draw() { * // Enable orbiting with the mouse. * orbitControl(); * * // Draw the image as a panorama (360˚ background). * panorama(img); * * // Add a soft ambient light. * ambientLight(50); * * // Add light from the image. * imageLight(img); * * // Style the sphere. * specularMaterial(20); * shininess(100); * noStroke(); * * // Draw the sphere. * sphere(30); * } */ fn.imageLight = function (img) { this._renderer.imageLight(img); }; /** * Creates an immersive 3D background. * * `panorama()` transforms images containing 360˚ content, such as maps or * HDRIs, into immersive 3D backgrounds that surround a sketch. Exploring the * space requires changing the camera's perspective with functions such as * orbitControl() or * camera(). * * @method panorama * @param {p5.Image} img 360˚ image to use as the background. * * @example * // Click and drag the mouse to view the scene from different angles. * * let img; * * async function setup() { * // Load an image and create a p5.Image object. * img = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(100, 100, WEBGL); * * describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape. The full landscape is viewable in 3D as the user drags the mouse.'); * } * * function draw() { * // Add the panorama. * panorama(img); * * // Enable orbiting with the mouse. * orbitControl(); * * // Use the image as a light source. * imageLight(img); * * // Style the sphere. * noStroke(); * specularMaterial(50); * shininess(200); * metalness(100); * * // Draw the sphere. * sphere(30); * } */ fn.panorama = function (img) { this.filter(this._renderer._getSphereMapping(img)); }; /** * Places an ambient and directional light in the scene. * The lights are set to ambientLight(128, 128, 128) and * directionalLight(128, 128, 128, 0, 0, -1). * * Note: lights need to be called (whether directly or indirectly) * within draw() to remain persistent in a looping program. * Placing them in setup() will cause them to only have an effect * the first time through the loop. * * @method lights * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click to turn on the lights. * * let isLit = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white box drawn against a gray background. The quality of the light changes when the user double-clicks.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Control the lights. * if (isLit === true) { * lights(); * } * * // Draw the box. * box(); * } * * // Turn on the lights when the user double-clicks. * function doubleClicked() { * isLit = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white box drawn against a gray background.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * ambientLight(128, 128, 128); * directionalLight(128, 128, 128, 0, 0, -1); * * // Draw the box. * box(); * } */ fn.lights = function () { this._assert3d('lights'); // Both specify gray by default. this._renderer.lights(); return this; }; /** * Sets the falloff rate for pointLight() * and spotLight(). * * A light’s falloff describes the intensity of its beam at a distance. For * example, a lantern has a slow falloff, a flashlight has a medium falloff, * and a laser pointer has a sharp falloff. * * `lightFalloff()` has three parameters, `constant`, `linear`, and * `quadratic`. They’re numbers used to calculate falloff at a distance, `d`, * as follows: * * `falloff = 1 / (constant + d * linear + (d * d) * quadratic)` * * Note: `constant`, `linear`, and `quadratic` should always be set to values * greater than 0. * * @method lightFalloff * @param {Number} constant constant value for calculating falloff. * @param {Number} linear linear value for calculating falloff. * @param {Number} quadratic quadratic value for calculating falloff. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click to change the falloff rate. * * let useFalloff = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A sphere drawn against a gray background. The intensity of the light changes when the user double-clicks.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Set the light falloff. * if (useFalloff === true) { * lightFalloff(2, 0, 0); * } * * // Add a white point light from the front. * pointLight(255, 255, 255, 0, 0, 100); * * // Style the sphere. * noStroke(); * * // Draw the sphere. * sphere(30); * } * * // Change the falloff value when the user double-clicks. * function doubleClicked() { * useFalloff = true; * } */ fn.lightFalloff = function ( constantAttenuation, linearAttenuation, quadraticAttenuation ) { this._assert3d('lightFalloff'); // p5._validateParameters('lightFalloff', arguments); this._renderer.lightFalloff( constantAttenuation, linearAttenuation, quadraticAttenuation ); return this; }; /** * Creates a light that shines from a point in one direction. * * Spot lights are like flashlights that shine in one direction creating a * cone of light. The shape of the cone can be controlled using the angle and * concentration parameters. A maximum of 5 spot lights can be active at once. * * There are eight ways to call `spotLight()` with parameters to set the * light’s color, position, direction. For example, * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light * at the origin `(0, 0, 0)` that points to the right `(1, 0, 0)`. * * The `angle` parameter is optional. It sets the radius of the light cone. * For example, `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16)` creates a * red `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right * `(1, 0, 0)` with an angle of `PI / 16` radians. By default, `angle` is * `PI / 3` radians. * * The `concentration` parameter is also optional. It focuses the light * towards the center of the light cone. For example, * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16, 50)` creates a red * `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right * `(1, 0, 0)` with an angle of `PI / 16` radians at concentration of 50. By * default, `concentration` is 100. * * @method spotLight * @param {Number} v1 red or hue value in the current * colorMode(). * @param {Number} v2 green or saturation value in the current * colorMode(). * @param {Number} v3 blue, brightness, or lightness value in the current * colorMode(). * @param {Number} x x-coordinate of the light. * @param {Number} y y-coordinate of the light. * @param {Number} z z-coordinate of the light. * @param {Number} rx x-component of light direction between -1 and 1. * @param {Number} ry y-component of light direction between -1 and 1. * @param {Number} rz z-component of light direction between -1 and 1. * @param {Number} [angle] angle of the light cone. Defaults to `PI / 3`. * @param {Number} [concentration] concentration of the light. Defaults to 100. * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click to adjust the spotlight. * * let isLit = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Control the spotlight. * if (isLit === true) { * // Add a red spot light that shines into the screen. * // Set its angle to PI / 32 radians. * spotLight(255, 0, 0, 0, 0, 100, 0, 0, -1, PI / 32); * } * * // Draw the sphere. * sphere(30); * } * * // Turn on the spotlight when the user double-clicks. * function doubleClicked() { * isLit = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click to adjust the spotlight. * * let isLit = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Control the spotlight. * if (isLit === true) { * // Add a red spot light that shines into the screen. * // Set its angle to PI / 3 radians (default). * // Set its concentration to 1000. * let c = color(255, 0, 0); * let position = createVector(0, 0, 100); * let direction = createVector(0, 0, -1); * spotLight(c, position, direction, PI / 3, 1000); * } * * // Draw the sphere. * sphere(30); * } * * // Turn on the spotlight when the user double-clicks. * function doubleClicked() { * isLit = true; * } */ /** * @method spotLight * @param {p5.Color|Number[]|String} color color as a p5.Color object, * an array of color values, or a CSS string. * @param {p5.Vector} position position of the light as a p5.Vector object. * @param {p5.Vector} direction direction of light as a p5.Vector object. * @param {Number} [angle] * @param {Number} [concentration] */ /** * @method spotLight * @param {Number} v1 * @param {Number} v2 * @param {Number} v3 * @param {p5.Vector} position * @param {p5.Vector} direction * @param {Number} [angle] * @param {Number} [concentration] */ /** * @method spotLight * @param {p5.Color|Number[]|String} color * @param {Number} x * @param {Number} y * @param {Number} z * @param {p5.Vector} direction * @param {Number} [angle] * @param {Number} [concentration] */ /** * @method spotLight * @param {p5.Color|Number[]|String} color * @param {p5.Vector} position * @param {Number} rx * @param {Number} ry * @param {Number} rz * @param {Number} [angle] * @param {Number} [concentration] */ /** * @method spotLight * @param {Number} v1 * @param {Number} v2 * @param {Number} v3 * @param {Number} x * @param {Number} y * @param {Number} z * @param {p5.Vector} direction * @param {Number} [angle] * @param {Number} [concentration] */ /** * @method spotLight * @param {Number} v1 * @param {Number} v2 * @param {Number} v3 * @param {p5.Vector} position * @param {Number} rx * @param {Number} ry * @param {Number} rz * @param {Number} [angle] * @param {Number} [concentration] */ /** * @method spotLight * @param {p5.Color|Number[]|String} color * @param {Number} x * @param {Number} y * @param {Number} z * @param {Number} rx * @param {Number} ry * @param {Number} rz * @param {Number} [angle] * @param {Number} [concentration] */ fn.spotLight = function ( v1, v2, v3, x, y, z, nx, ny, nz, angle, concentration ) { this._assert3d('spotLight'); // p5._validateParameters('spotLight', arguments); this._renderer.spotLight(...arguments); return this; }; /** * Removes all lights from the sketch. * * Calling `noLights()` removes any lights created with * lights(), * ambientLight(), * directionalLight(), * pointLight(), or * spotLight(). These functions may be called * after `noLights()` to create a new lighting scheme. * * @method noLights * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('Two spheres drawn against a gray background. The top sphere is white and the bottom sphere is red.'); * } * * function draw() { * background(50); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on the lights. * lights(); * * // Style the spheres. * noStroke(); * * // Draw the top sphere. * push(); * translate(0, -25, 0); * sphere(20); * pop(); * * // Turn off the lights. * noLights(); * * // Add a red directional light that points into the screen. * directionalLight(255, 0, 0, 0, 0, -1); * * // Draw the bottom sphere. * push(); * translate(0, 25, 0); * sphere(20); * pop(); * } */ fn.noLights = function (...args) { this._assert3d('noLights'); // p5._validateParameters('noLights', args); this._renderer.noLights(); return this; }; Renderer3D.prototype.ambientLight = function(v1, v2, v3, a) { const color = this._pInst.color(...arguments); this.states.setValue('ambientLightColors', [...this.states.ambientLightColors]); this.states.ambientLightColors.push( color._array[0], color._array[1], color._array[2] ); this.states.setValue('enableLighting', true); }; Renderer3D.prototype.specularColor = function(v1, v2, v3) { const color = this._pInst.color(...arguments); this.states.setValue('specularColors', [ color._array[0], color._array[1], color._array[2] ]); }; Renderer3D.prototype.directionalLight = function(v1, v2, v3, x, y, z) { let color; if (v1 instanceof Color) { color = v1; } else { color = this._pInst.color(v1, v2, v3); } let _x, _y, _z; const v = arguments[arguments.length - 1]; if (typeof v === 'number') { _x = arguments[arguments.length - 3]; _y = arguments[arguments.length - 2]; _z = arguments[arguments.length - 1]; } else { _x = v.x; _y = v.y; _z = v.z; } // normalize direction const l = Math.sqrt(_x * _x + _y * _y + _z * _z); this.states.setValue('directionalLightDirections', [...this.states.directionalLightDirections]); this.states.directionalLightDirections.push(_x / l, _y / l, _z / l); this.states.setValue('directionalLightDiffuseColors', [...this.states.directionalLightDiffuseColors]); this.states.directionalLightDiffuseColors.push( color._array[0], color._array[1], color._array[2] ); this.states.setValue('directionalLightSpecularColors', [...this.states.directionalLightSpecularColors]); Array.prototype.push.apply( this.states.directionalLightSpecularColors, this.states.specularColors ); this.states.setValue('enableLighting', true); }; Renderer3D.prototype.pointLight = function(v1, v2, v3, x, y, z) { let color; if (v1 instanceof Color) { color = v1; } else { color = this._pInst.color(v1, v2, v3); } let _x, _y, _z; const v = arguments[arguments.length - 1]; if (typeof v === 'number') { _x = arguments[arguments.length - 3]; _y = arguments[arguments.length - 2]; _z = arguments[arguments.length - 1]; } else { _x = v.x; _y = v.y; _z = v.z; } this.states.setValue('pointLightPositions', [...this.states.pointLightPositions]); this.states.pointLightPositions.push(_x, _y, _z); this.states.setValue('pointLightDiffuseColors', [...this.states.pointLightDiffuseColors]); this.states.pointLightDiffuseColors.push( color._array[0], color._array[1], color._array[2] ); this.states.setValue('pointLightSpecularColors', [...this.states.pointLightSpecularColors]); Array.prototype.push.apply( this.states.pointLightSpecularColors, this.states.specularColors ); this.states.setValue('enableLighting', true); }; Renderer3D.prototype.imageLight = function(img) { // activeImageLight property is checked by _setFillUniforms // for sending uniforms to the fillshader this.states.setValue('activeImageLight', img); this.states.setValue('enableLighting', true); // Make sure textures are cached this.makeDiffusedTexture(img); this.makeSpecularTexture(img); }; Renderer3D.prototype.lights = function() { const grayColor = this._pInst.color('rgb(128,128,128)'); this.ambientLight(grayColor); this.directionalLight(grayColor, 0, 0, -1); }; Renderer3D.prototype.lightFalloff = function( constantAttenuation, linearAttenuation, quadraticAttenuation ) { if (constantAttenuation < 0) { constantAttenuation = 0; console.warn( 'Value of constant argument in lightFalloff() should be never be negative. Set to 0.' ); } if (linearAttenuation < 0) { linearAttenuation = 0; console.warn( 'Value of linear argument in lightFalloff() should be never be negative. Set to 0.' ); } if (quadraticAttenuation < 0) { quadraticAttenuation = 0; console.warn( 'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.' ); } if ( constantAttenuation === 0 && (linearAttenuation === 0 && quadraticAttenuation === 0) ) { constantAttenuation = 1; console.warn( 'Either one of the three arguments in lightFalloff() should be greater than zero. Set constant argument to 1.' ); } this.states.setValue('constantAttenuation', constantAttenuation); this.states.setValue('linearAttenuation', linearAttenuation); this.states.setValue('quadraticAttenuation', quadraticAttenuation); }; Renderer3D.prototype.spotLight = function( v1, v2, v3, x, y, z, nx, ny, nz, angle, concentration ) { if (this.states.spotLightDiffuseColors.length / 3 >= 4) return; let color, position, direction; const length = arguments.length; switch (length) { case 11: case 10: color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = new Vector(nx, ny, nz); break; case 9: if (v1 instanceof Color) { color = v1; position = new Vector(v2, v3, x); direction = new Vector(y, z, nx); angle = ny; concentration = nz; } else if (x instanceof Vector) { color = this._pInst.color(v1, v2, v3); position = x; direction = new Vector(y, z, nx); angle = ny; concentration = nz; } else if (nx instanceof Vector) { color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = nx; angle = ny; concentration = nz; } else { color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = new Vector(nx, ny, nz); } break; case 8: if (v1 instanceof Color) { color = v1; position = new Vector(v2, v3, x); direction = new Vector(y, z, nx); angle = ny; } else if (x instanceof Vector) { color = this._pInst.color(v1, v2, v3); position = x; direction = new Vector(y, z, nx); angle = ny; } else { color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = nx; angle = ny; } break; case 7: if (v1 instanceof Color && v2 instanceof Vector) { color = v1; position = v2; direction = new Vector(v3, x, y); angle = z; concentration = nx; } else if (v1 instanceof Color && y instanceof Vector) { color = v1; position = new Vector(v2, v3, x); direction = y; angle = z; concentration = nx; } else if (x instanceof Vector && y instanceof Vector) { color = this._pInst.color(v1, v2, v3); position = x; direction = y; angle = z; concentration = nx; } else if (v1 instanceof Color) { color = v1; position = new Vector(v2, v3, x); direction = new Vector(y, z, nx); } else if (x instanceof Vector) { color = this._pInst.color(v1, v2, v3); position = x; direction = new Vector(y, z, nx); } else { color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = nx; } break; case 6: if (x instanceof Vector && y instanceof Vector) { color = this._pInst.color(v1, v2, v3); position = x; direction = y; angle = z; } else if (v1 instanceof Color && y instanceof Vector) { color = v1; position = new Vector(v2, v3, x); direction = y; angle = z; } else if (v1 instanceof Color && v2 instanceof Vector) { color = v1; position = v2; direction = new Vector(v3, x, y); angle = z; } break; case 5: if ( v1 instanceof Color && v2 instanceof Vector && v3 instanceof Vector ) { color = v1; position = v2; direction = v3; angle = x; concentration = y; } else if (x instanceof Vector && y instanceof Vector) { color = this._pInst.color(v1, v2, v3); position = x; direction = y; } else if (v1 instanceof Color && y instanceof Vector) { color = v1; position = new Vector(v2, v3, x); direction = y; } else if (v1 instanceof Color && v2 instanceof Vector) { color = v1; position = v2; direction = new Vector(v3, x, y); } break; case 4: color = v1; position = v2; direction = v3; angle = x; break; case 3: color = v1; position = v2; direction = v3; break; default: console.warn( `Sorry, input for spotlight() is not in prescribed format. Too ${ length < 3 ? 'few' : 'many' } arguments were provided` ); return; } this.states.setValue('spotLightDiffuseColors', [ ...this.states.spotLightDiffuseColors, color._array[0], color._array[1], color._array[2] ]); this.states.setValue('spotLightSpecularColors', [ ...this.states.spotLightSpecularColors, ...this.states.specularColors ]); this.states.setValue('spotLightPositions', [ ...this.states.spotLightPositions, position.x, position.y, position.z ]); direction.normalize(); this.states.setValue('spotLightDirections', [ ...this.states.spotLightDirections, direction.x, direction.y, direction.z ]); if (angle === undefined) { angle = Math.PI / 3; } if (concentration !== undefined && concentration < 1) { concentration = 1; console.warn( 'Value of concentration needs to be greater than 1. Setting it to 1' ); } else if (concentration === undefined) { concentration = 100; } angle = this._pInst._toRadians(angle); this.states.setValue('spotLightAngle', [...this.states.spotLightAngle, Math.cos(angle)]); this.states.setValue('spotLightConc', [...this.states.spotLightConc, concentration]); this.states.setValue('enableLighting', true); }; Renderer3D.prototype.noLights = function() { this.states.setValue('activeImageLight', null); this.states.setValue('enableLighting', false); this.states.setValue('ambientLightColors', []); this.states.setValue('specularColors', [1, 1, 1]); this.states.setValue('directionalLightDirections', []); this.states.setValue('directionalLightDiffuseColors', []); this.states.setValue('directionalLightSpecularColors', []); this.states.setValue('pointLightPositions', []); this.states.setValue('pointLightDiffuseColors', []); this.states.setValue('pointLightSpecularColors', []); this.states.setValue('spotLightPositions', []); this.states.setValue('spotLightDirections', []); this.states.setValue('spotLightDiffuseColors', []); this.states.setValue('spotLightSpecularColors', []); this.states.setValue('spotLightAngle', []); this.states.setValue('spotLightConc', []); this.states.setValue('constantAttenuation', 1); this.states.setValue('linearAttenuation', 0); this.states.setValue('quadraticAttenuation', 0); this.states.setValue('_useShininess', 1); this.states.setValue('_useMetalness', 0); }; } if(typeof p5 !== 'undefined'){ light(p5, p5.prototype); } /** * This module defines the p5.Shader class * @module 3D * @submodule Material * @for p5 * @requires core */ const TypedArray = Object.getPrototypeOf(Uint8Array); class Shader { constructor(renderer, vertSrc, fragSrc, options = {}) { this._renderer = renderer; this._vertSrc = vertSrc; this._fragSrc = fragSrc; this._vertShader = -1; this._fragShader = -1; this._glProgram = 0; this._loadedAttributes = false; this.attributes = {}; this._loadedUniforms = false; this.uniforms = {}; this._bound = false; this.samplers = []; this.hooks = { // These should be passed in by `.modify()` instead of being manually // passed in. // Stores uniforms + default values. uniforms: options.uniforms || {}, // Stores custom uniform + helper declarations as a string. declarations: options.declarations, // Stores an array of variable names + types passed between the vertex and fragment shader varyingVariables: options.varyingVariables || [], // Stores helper functions to prepend to shaders. helpers: options.helpers || {}, // Stores the hook implementations vertex: options.vertex || {}, fragment: options.fragment || {}, hookAliases: options.hookAliases || {}, // Stores whether or not the hook implementation has been modified // from the default. This is supplied automatically by calling // yourShader.modify(...). modified: { vertex: (options.modified && options.modified.vertex) || {}, fragment: (options.modified && options.modified.fragment) || {} } }; } hookTypes(hookName) { return this._renderer.getShaderHookTypes(this, hookName); } shaderSrc(src, shaderType) { return this._renderer.populateHooks(this, src, shaderType); } /** * Shaders are written in GLSL, but * there are different versions of GLSL that it might be written in. * * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. * * @returns {String} The GLSL version used by the shader. */ version() { const match = /#version (.+)$/.exec(this.vertSrc()); if (match) { return match[1]; } else { return '100 es'; } } vertSrc() { return this.shaderSrc(this._vertSrc, 'vertex'); } fragSrc() { return this.shaderSrc(this._fragSrc, 'fragment'); } /** * Logs the hooks available in this shader, and their current implementation. * * Each shader may let you override bits of its behavior. Each bit is called * a *hook.* A hook is either for the *vertex* shader, if it affects the * position of vertices, or in the *fragment* shader, if it affects the pixel * color. This method logs those values to the console, letting you know what * you are able to use in a call to * `modify()`. * * For example, this shader will produce the following output: * * ```js * myShader = baseMaterialShader().modify({ * declarations: 'uniform float time;', * 'vec3 getWorldPosition': `(vec3 pos) { * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); * return pos; * }` * }); * myShader.inspectHooks(); * ``` * * ``` * ==== Vertex shader hooks: ==== * void beforeVertex() {} * vec3 getLocalPosition(vec3 position) { return position; } * [MODIFIED] vec3 getWorldPosition(vec3 pos) { * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); * return pos; * } * vec3 getLocalNormal(vec3 normal) { return normal; } * vec3 getWorldNormal(vec3 normal) { return normal; } * vec2 getUV(vec2 uv) { return uv; } * vec4 getVertexColor(vec4 color) { return color; } * void afterVertex() {} * * ==== Fragment shader hooks: ==== * void beforeFragment() {} * Inputs getPixelInputs(Inputs inputs) { return inputs; } * vec4 combineColors(ColorComponents components) { * vec4 color = vec4(0.); * color.rgb += components.diffuse * components.baseColor; * color.rgb += components.ambient * components.ambientColor; * color.rgb += components.specular * components.specularColor; * color.rgb += components.emissive; * color.a = components.opacity; * return color; * } * vec4 getFinalColor(vec4 color) { return color; } * void afterFragment() {} * ``` * * @beta */ inspectHooks() { console.log('==== Vertex shader hooks: ===='); for (const key in this.hooks.vertex) { console.log( (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + key + this.hooks.vertex[key] ); } console.log(''); console.log('==== Fragment shader hooks: ===='); for (const key in this.hooks.fragment) { console.log( (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + key + this.hooks.fragment[key] ); } console.log(''); console.log('==== Helper functions: ===='); for (const key in this.hooks.helpers) { console.log(key + this.hooks.helpers[key]); } } /** * Returns a new shader, based on the original, but with custom snippets * of shader code replacing default behaviour. * * Each shader may let you override bits of its behavior. Each bit is called * a *hook.* For example, a hook can let you adjust positions of vertices, or * the color of a pixel. You can inspect the different hooks available by calling * `yourShader.inspectHooks()`. You can * also read the reference for the default material, normal material, color, line, and point shaders to * see what hooks they have available. * * `modify()` can be passed a function as a parameter. Inside, you can override hooks * by calling them as functions. Each hook will take in a callback that takes in inputs * and is expected to return an output. For example, here is a function that changes the * material color to red: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseMaterialShader().modify(() => { * getPixelInputs((inputs) => { * inputs.color = [inputs.texCoord, 0, 1]; * return inputs; * }); * }); * } * * function draw() { * background(255); * noStroke(); * shader(myShader); // Apply the custom shader * plane(width, height); // Draw a plane with the shader applied * } * ``` * * In addition to calling hooks, you can create uniforms, which are special variables * used to pass data from p5.js into the shader. They can be created by calling `uniform` + the * type of the data, such as `uniformFloat` for a number of `uniformVector2` for a two-component vector. * They take in a function that returns the data for the variable. You can then reference these * variables in your hooks, and their values will update every time you apply * the shader with the result of your function. * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseMaterialShader().modify(() => { * // Get the current time from p5.js * let t = uniformFloat(() => millis()); * * getPixelInputs((inputs) => { * inputs.color = [ * inputs.texCoord, * sin(t * 0.01) / 2 + 0.5, * 1, * ]; * return inputs; * }); * }); * } * * function draw() { * background(255); * noStroke(255); * shader(myShader); // Apply the custom shader * plane(width, height); // Draw a plane with the shader applied * } * ``` * * p5.strands functions are special, since they get turned into a shader instead of being * run like the rest of your code. They only have access to p5.js functions, and variables * you declare inside the `modify` callback. If you need access to local variables, you * can pass them into `modify` with an optional second parameter, `variables`. These will * then be passed into your function as an argument. If you are * using instance mode, you will need to pass your sketch object in this way. * * If you are also using a build system for your sketch, variable names may be changed as * part of minification. When creating a uniform, you can pass the name of the uniform in * as a first parameter to ensure it doesn't get changed. * * ```js example * new p5((sketch) => { * let myShader; * * sketch.setup = function() { * sketch.createCanvas(200, 200, sketch.WEBGL); * myShader = sketch.baseMaterialShader().modify(({ sketch }) => { * let b = sketch.uniformFloat('b'); * sketch.getPixelInputs((inputs) => { * inputs.color = [inputs.texCoord, b, 1]; * return inputs; * }); * }, { sketch }); * } * * sketch.draw = function() { * sketch.background(255); * sketch.noStroke(); * myShader.setUniform('b', 0.5); * sketch.shader(myShader); // Apply the custom shader * sketch.plane(sketch.width, sketch.height); // Draw a plane with the shader applied * } * }); * ``` * * You can also write GLSL directly in `modify` if you need direct access. To do so, * `modify()` takes one parameter, `hooks`, an object with the hooks you want * to override. Each key of the `hooks` object is the name * of a hook, and the value is a string with the GLSL code for your hook. * * If you supply functions that aren't existing hooks, they will get added at the start of * the shader as helper functions so that you can use them in your hooks. * * To add new uniforms to your shader, you can pass in a `uniforms` object containing * the type and name of the uniform as the key, and a default value or function returning * a default value as its value. These will be automatically set when the shader is set * with `shader(yourShader)`. * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseMaterialShader().modify({ * uniforms: { * 'float time': () => millis() // Uniform for time * }, * 'Vertex getWorldInputs': `(Vertex inputs) { * inputs.position.y += * 20. * sin(time * 0.001 + inputs.position.x * 0.05); * return inputs; * }` * }); * } * * function draw() { * background(255); * shader(myShader); // Apply the custom shader * lights(); // Enable lighting * noStroke(); // Disable stroke * fill('red'); // Set fill color to red * sphere(50); // Draw a sphere with the shader applied * } * ``` * * You can also add a `declarations` key, where the value is a GLSL string declaring * custom uniform variables, globals, and functions shared * between hooks. To add declarations just in a vertex or fragment shader, add * `vertexDeclarations` and `fragmentDeclarations` keys. * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseMaterialShader().modify({ * // Manually specifying a uniform * declarations: 'uniform float time;', * 'Vertex getWorldInputs': `(Vertex inputs) { * inputs.position.y += * 20. * sin(time * 0.001 + inputs.position.x * 0.05); * return inputs; * }` * }); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); * sphere(50); * } * ``` * * @beta * @param {Function} callback A function with p5.strands code to modify the shader. * @param {Object} [variables] An optional object with local variables p5.strands * should have access to. * @returns {p5.Shader} */ /** * @param {Object} [hooks] The hooks in the shader to replace. * @returns {p5.Shader} */ modify(hooks) { // p5._validateParameters('p5.Shader.modify', arguments); const newHooks = { vertex: {}, fragment: {}, helpers: {} }; for (const key in hooks) { if (key === 'declarations') continue; if (key === 'uniforms') continue; if (key === 'varyingVariables') continue; if (key === 'vertexDeclarations') { newHooks.vertex.declarations = (newHooks.vertex.declarations || '') + '\n' + hooks[key]; } else if (key === 'fragmentDeclarations') { newHooks.fragment.declarations = (newHooks.fragment.declarations || '') + '\n' + hooks[key]; } else if (this.hooks.vertex[key]) { newHooks.vertex[key] = hooks[key]; } else if (this.hooks.fragment[key]) { newHooks.fragment[key] = hooks[key]; } else { newHooks.helpers[key] = hooks[key]; } } const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); for (const key in newHooks.vertex || {}) { if (key === 'declarations') continue; modifiedVertex[key] = true; } for (const key in newHooks.fragment || {}) { if (key === 'declarations') continue; modifiedFragment[key] = true; } return new Shader(this._renderer, this._vertSrc, this._fragSrc, { declarations: (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), varyingVariables: (hooks.varyingVariables || []).concat(this.hooks.varyingVariables || []), fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), hookAliases: Object.assign({}, this.hooks.hookAliases, newHooks.hookAliases || {}), modified: { vertex: modifiedVertex, fragment: modifiedFragment } }); } /** * Creates, compiles, and links the shader based on its * sources for the vertex and fragment shaders (provided * to the constructor). Populates known attributes and * uniforms from the shader. * @chainable * @private */ init() { // If the shader is uninitialized or context was lost if (!this._initialized) { try { this._renderer._initShader(this); // Backend-specific shader init } catch (err) { throw new Error( `Whoops! Something went wrong initializing the shader:\n${err.message || err}` ); } this._loadAttributes(); this._loadUniforms(); this._renderer._finalizeShader(this); this._initialized = true; } return this; } /** * @private */ setDefaultUniforms() { for (const key in this.hooks.uniforms) { const name = this._renderer.uniformNameFromHookKey(key); const initializer = this.hooks.uniforms[key]; let value; if (initializer instanceof Function) { value = initializer(); } else { value = initializer; } if (value !== undefined && value !== null) { this.setUniform(name, value); } } } /** * Copies the shader from one drawing context to another. * * Each `p5.Shader` object must be compiled by calling * shader() before it can run. Compilation happens * in a drawing context which is usually the main canvas or an instance of * p5.Graphics. A shader can only be used in the * context where it was compiled. The `copyToContext()` method compiles the * shader again and copies it to another drawing context where it can be * reused. * * The parameter, `context`, is the drawing context where the shader will be * used. The shader can be copied to an instance of * p5.Graphics, as in * `myShader.copyToContext(pg)`. The shader can also be copied from a * p5.Graphics object to the main canvas using * the `p5.instance` variable, as in `myShader.copyToContext(p5.instance)`. * * Note: A p5.Shader object created with * createShader(), * createFilterShader(), or * loadShader() * can be used directly with a p5.Framebuffer * object created with * createFramebuffer(). Both objects * have the same context as the main canvas. * * @param {p5|p5.Graphics} context WebGL context for the copied shader. * @returns {p5.Shader} new shader compiled for the target context. * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision mediump float; * varying vec2 vTexCoord; * * void main() { * vec2 uv = vTexCoord; * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); * gl_FragColor = vec4(color, 1.0);\ * } * `; * * let pg; * * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Shader object. * let original = createShader(vertSrc, fragSrc); * * // Compile the p5.Shader object. * shader(original); * * // Create a p5.Graphics object. * pg = createGraphics(50, 50, WEBGL); * * // Copy the original shader to the p5.Graphics object. * let copied = original.copyToContext(pg); * * // Apply the copied shader to the p5.Graphics object. * pg.shader(copied); * * // Style the display surface. * pg.noStroke(); * * // Add a display surface for the shader. * pg.plane(50, 50); * * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); * } * * function draw() { * background(200); * * // Draw the p5.Graphics object to the main canvas. * image(pg, -25, -25); * } * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision mediump float; * * varying vec2 vTexCoord; * * void main() { * vec2 uv = vTexCoord; * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); * gl_FragColor = vec4(color, 1.0); * } * `; * * let copied; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Graphics object. * let pg = createGraphics(25, 25, WEBGL); * * // Create a p5.Shader object. * let original = pg.createShader(vertSrc, fragSrc); * * // Compile the p5.Shader object. * pg.shader(original); * * // Copy the original shader to the main canvas. * copied = original.copyToContext(p5.instance); * * // Apply the copied shader to the main canvas. * shader(copied); * * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); * } * * function draw() { * background(200); * * // Rotate around the x-, y-, and z-axes. * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * rotateZ(frameCount * 0.01); * * // Draw the box. * box(50); * } */ copyToContext(context) { const shader = new Shader( context._renderer, this._vertSrc, this._fragSrc ); shader.ensureCompiledOnContext(context._renderer); return shader; } /** * @private */ ensureCompiledOnContext(context) { if (this._glProgram !== 0 && this._renderer !== context) { throw new Error( 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' ); } else if (this._glProgram === 0) { this._renderer = context?._renderer?.filterRenderer?._renderer || context; this.init(); } } /** * Queries the active attributes for this shader and loads * their names and locations into the attributes array. * @private */ _loadAttributes() { if (this._loadedAttributes) { return; } this.attributes = this._renderer._getShaderAttributes(this); this._loadedAttributes = true; } /** * Queries the active uniforms for this shader and loads * their names and locations into the uniforms array. * @private */ _loadUniforms() { if (this._loadedUniforms) { return; } this.uniforms = {}; this.samplers = []; const uniformMetadata = this._renderer.getUniformMetadata(this); for (const meta of uniformMetadata) { const uniform = { ...meta, _cachedData: undefined, }; if (uniform.isSampler) { this.samplers.push(uniform); } this.uniforms[uniform.name] = uniform; } this._loadedUniforms = true; } /** * initializes (if needed) and binds the shader program. * @private */ bindShader(shaderType, options) { if (this.shaderType && this.shaderType !== shaderType) { throw new Error( `You've already used this shader as a ${this.shaderType} shader, but are now using it as a ${shaderType}.` ); } this.shaderType = shaderType; this.init(); if (!this._bound) { this.useProgram(options); this._bound = true; } } /** * @chainable * @private */ unbindShader() { if (this._bound) { this.unbindTextures(); this._bound = false; } return this; } /** * @private */ bindTextures() { const empty = this._renderer._getEmptyTexture(); for (const uniform of this.samplers) { if (uniform.noData) continue; let tex = uniform.texture; if ( tex === undefined || ( // Make sure we unbind a framebuffer uniform if it's the same // framebuffer that is actvely being drawn to in order to // prevent a feedback cycle tex.isFramebufferTexture && !tex.src.framebuffer.antialias && tex.src.framebuffer === this._renderer.activeFramebuffer() ) ) { // user hasn't yet supplied a texture for this slot. // (or there may not be one--maybe just lighting), // so we supply a default texture instead. uniform.texture = tex = empty; } this._renderer._updateTexture(uniform, tex); } } /** * @private */ unbindTextures() { for (const uniform of this.samplers) { if (uniform.texture?.isFramebufferTexture) { this._renderer._unbindFramebufferTexture(uniform); } } } /** * @chainable * @private */ useProgram(options) { if (this._renderer._curShader !== this) { this._renderer._useShader(this); this._renderer._curShader = this; } return this; } /** * Sets the shader’s uniform (global) variables. * * Shader programs run on the computer’s graphics processing unit (GPU). * They live in part of the computer’s memory that’s completely separate * from the sketch that runs them. Uniforms are global variables within a * shader program. They provide a way to pass values from a sketch running * on the CPU to a shader program running on the GPU. * * The first parameter, `uniformName`, is a string with the uniform’s name. * For the shader above, `uniformName` would be `'r'`. * * The second parameter, `data`, is the value that should be used to set the * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set * the `r` uniform in the shader above to `0.5`. data should match the * uniform’s type. Numbers, strings, booleans, arrays, and many types of * images can all be passed to a shader with `setUniform()`. * * @chainable * @param {String} uniformName name of the uniform. Must match the name * used in the vertex and fragment shaders. * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} * data value to assign to the uniform. Must match the uniform’s data type. * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision mediump float; * * uniform float r; * * void main() { * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); * } * `; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * let myShader = createShader(vertSrc, fragSrc); * * // Apply the p5.Shader object. * shader(myShader); * * // Set the r uniform to 0.5. * myShader.setUniform('r', 0.5); * * // Style the drawing surface. * noStroke(); * * // Add a plane as a drawing surface for the shader. * plane(100, 100); * * describe('A cyan square.'); * } * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision mediump float; * * uniform float r; * * void main() { * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); * } * `; * * let myShader; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * myShader = createShader(vertSrc, fragSrc); * * // Compile and apply the p5.Shader object. * shader(myShader); * * describe('A square oscillates color between cyan and white.'); * } * * function draw() { * background(200); * * // Style the drawing surface. * noStroke(); * * // Update the r uniform. * let nextR = 0.5 * (sin(frameCount * 0.01) + 1); * myShader.setUniform('r', nextR); * * // Add a plane as a drawing surface. * plane(100, 100); * } * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision highp float; * uniform vec2 p; * uniform float r; * const int numIterations = 500; * varying vec2 vTexCoord; * * void main() { * vec2 c = p + gl_FragCoord.xy * r; * vec2 z = c; * float n = 0.0; * * for (int i = numIterations; i > 0; i--) { * if (z.x * z.x + z.y * z.y > 4.0) { * n = float(i) / float(numIterations); * break; * } * * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; * } * * gl_FragColor = vec4( * 0.5 - cos(n * 17.0) / 2.0, * 0.5 - cos(n * 13.0) / 2.0, * 0.5 - cos(n * 23.0) / 2.0, * 1.0 * ); * } * `; * * let mandelbrot; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * mandelbrot = createShader(vertSrc, fragSrc); * * // Compile and apply the p5.Shader object. * shader(mandelbrot); * * // Set the shader uniform p to an array. * // p is the center point of the Mandelbrot image. * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); * * describe('A fractal image zooms in and out of focus.'); * } * * function draw() { * // Set the shader uniform r to a value that oscillates * // between 0 and 0.005. * // r is the size of the image in Mandelbrot-space. * let radius = 0.005 * (sin(frameCount * 0.01) + 1); * mandelbrot.setUniform('r', radius); * * // Style the drawing surface. * noStroke(); * * // Add a plane as a drawing surface. * plane(100, 100); * } */ setUniform(uniformName, data) { this.init(); const uniform = this.uniforms[uniformName]; if (!uniform) { return; } if (uniform.isArray) { if ( uniform._cachedData && this._renderer._arraysEqual(uniform._cachedData, data) ) { return; } else { uniform._cachedData = data.slice(0); } } else if (uniform._cachedData && uniform._cachedData === data) { return; } else { if (Array.isArray(data) || data instanceof TypedArray) { if (uniform._cachedData && this._renderer._arraysEqual(uniform._cachedData, data)) { return; } uniform._cachedData = data.slice(0); } else { if (uniform._cachedData === data) return; uniform._cachedData = data; } } this._renderer.updateUniformValue(this, uniform, data); } /** * @chainable * @private */ enableAttrib(attr, size, type, normalized, stride, offset) { if (attr) { if ( typeof IS_MINIFIED === 'undefined' && this.attributes[attr.name] !== attr ) { console.warn( `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.` ); } if (attr.location !== -1) { this._renderer._enableAttrib(this, attr, size, type, normalized, stride, offset); } } return this; } } function shader(p5, fn){ /** * A class to describe a shader program. * * Each `p5.Shader` object contains a shader program that runs on the graphics * processing unit (GPU). Shaders can process many pixels or vertices at the * same time, making them fast for many graphics tasks. They’re written in a * language called * GLSL * and run along with the rest of the code in a sketch. * * A shader program consists of two files, a vertex shader and a fragment * shader. The vertex shader affects where 3D geometry is drawn on the screen * and the fragment shader affects color. Once the `p5.Shader` object is * created, it can be used with the shader() * function, as in `shader(myShader)`. * * A shader can optionally describe *hooks,* which are functions in GLSL that * users may choose to provide to customize the behavior of the shader. For the * vertex or the fragment shader, users can pass in an object where each key is * the type and name of a hook function, and each value is a string with the * parameter list and default implementation of the hook. For example, to let users * optionally run code at the start of the vertex shader, the options object could * include: * * ```js * { * vertex: { * 'void beforeVertex': '() {}' * } * } * ``` * * Then, in your vertex shader source, you can run a hook by calling a function * with the same name prefixed by `HOOK_`: * * ```glsl * void main() { * HOOK_beforeVertex(); * // Add the rest ofy our shader code here! * } * ``` * * Note: createShader(), * createFilterShader(), and * loadShader() are the recommended ways to * create an instance of this class. * * @class p5.Shader * @constructor * @param {p5.RendererGL} renderer WebGL context for this shader. * @param {String} vertSrc source code for the vertex shader program. * @param {String} fragSrc source code for the fragment shader program. * @param {Object} [options] An optional object describing how this shader can * be augmented with hooks. It can include: * - `vertex`: An object describing the available vertex shader hooks. * - `fragment`: An object describing the available frament shader hooks. * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision highp float; * * void main() { * // Set each pixel's RGBA value to yellow. * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); * } * `; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * let myShader = createShader(vertSrc, fragSrc); * * // Apply the p5.Shader object. * shader(myShader); * * // Style the drawing surface. * noStroke(); * * // Add a plane as a drawing surface. * plane(100, 100); * * describe('A yellow square.'); * } * * @example * // Note: A "uniform" is a global variable within a shader program. * * let mandelbrot; * * async function setup() { * mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag'); * createCanvas(100, 100, WEBGL); * * // Use the p5.Shader object. * shader(mandelbrot); * * // Set the shader uniform p to an array. * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); * * describe('A fractal image zooms in and out of focus.'); * } * * function draw() { * // Set the shader uniform r to a value that oscillates between 0 and 2. * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); * * // Add a quad as a display surface for the shader. * quad(-1, -1, 1, -1, 1, 1, -1, 1); * } */ p5.Shader = Shader; } if(typeof p5 !== 'undefined'){ shader(p5); } /** * @module 3D * @submodule Material * @for p5 * @requires core */ async function urlToStrandsCallback(url) { const src = await fetch(url).then((res) => res.text()); return new Function(src); } function withGlobalStrands(p5, cb) { const prevGlobalStrands = p5._runStrandsInGlobalMode; p5._runStrandsInGlobalMode = true; try { return cb(); } finally { p5._runStrandsInGlobalMode = prevGlobalStrands; } } function material(p5, fn) { /** * Loads vertex and fragment shaders to create a * p5.Shader object. * * Shaders are programs that run on the graphics processing unit (GPU). They * can process many pixels at the same time, making them fast for many * graphics tasks. They’re written in a language called * GLSL * and run along with the rest of the code in a sketch. * * Once the p5.Shader object is created, it can be * used with the shader() function, as in * `shader(myShader)`. A shader program consists of two files, a vertex shader * and a fragment shader. The vertex shader affects where 3D geometry is drawn * on the screen and the fragment shader affects color. * * `loadShader()` loads the vertex and fragment shaders from their `.vert` and * `.frag` files. For example, calling * `loadShader('assets/shader.vert', 'assets/shader.frag')` loads both * required shaders and returns a p5.Shader object. * * The third parameter, `successCallback`, is optional. If a function is * passed, it will be called once the shader has loaded. The callback function * can use the new p5.Shader object as its * parameter. The return value of the `successCallback()` function will be used * as the final return value of `loadShader()`. * * The fourth parameter, `failureCallback`, is also optional. If a function is * passed, it will be called if the shader fails to load. The callback * function can use the event error as its parameter. The return value of the ` * failureCallback()` function will be used as the final return value of `loadShader()`. * * This function returns a `Promise` and should be used in an `async` setup with * `await`. See the examples for the usage syntax. * * Note: Shaders can only be used in WebGL mode. * * @method loadShader * @param {String|Request} vertFilename path of the vertex shader to be loaded. * @param {String|Request} fragFilename path of the fragment shader to be loaded. * @param {Function} [successCallback] function to call once the shader is loaded. Can be passed the * p5.Shader object. * @param {Function} [failureCallback] function to call if the shader fails to load. Can be passed an * `Error` event object. * @return {Promise} new shader created from the vertex and fragment shader files. * * @example * // Note: A "uniform" is a global variable within a shader program. * * let mandelbrot; * * // Load the shader and create a p5.Shader object. * async function setup() { * mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag'); * * createCanvas(100, 100, WEBGL); * * // Compile and apply the p5.Shader object. * shader(mandelbrot); * * // Set the shader uniform p to an array. * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); * * // Set the shader uniform r to the value 1.5. * mandelbrot.setUniform('r', 1.5); * * // Add a quad as a display surface for the shader. * quad(-1, -1, 1, -1, 1, 1, -1, 1); * * describe('A black fractal image on a magenta background.'); * } * * @example * // Note: A "uniform" is a global variable within a shader program. * * let mandelbrot; * * // Load the shader and create a p5.Shader object. * async function setup() { * mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag'); * * createCanvas(100, 100, WEBGL); * * // Use the p5.Shader object. * shader(mandelbrot); * * // Set the shader uniform p to an array. * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); * * describe('A fractal image zooms in and out of focus.'); * } * * function draw() { * // Set the shader uniform r to a value that oscillates between 0 and 2. * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); * * // Add a quad as a display surface for the shader. * quad(-1, -1, 1, -1, 1, 1, -1, 1); * } */ fn.loadShader = async function ( vertFilename, fragFilename, successCallback, failureCallback, ) { // p5._validateParameters('loadShader', arguments); const loadedShader = new Shader(); try { loadedShader._vertSrc = (await request(vertFilename, "text")).data; loadedShader._fragSrc = (await request(fragFilename, "text")).data; if (successCallback) { return successCallback(loadedShader) || loadedShader; } else { return loadedShader; } } catch (err) { if (failureCallback) { return failureCallback(err); } else { throw err; } } }; /** * Creates a new p5.Shader object using GLSL. * * If you are interested in writing shaders, consider using p5.strands shaders using * `buildMaterialShader`, * `buildStrokeShader`, or * `buildFilterShader`. * With p5.strands, you can modify existing shaders using JavaScript. With * `createShader`, shaders are made from scratch, and are written in GLSL. This * will be most useful for advanced cases, and for authors of add-on libraries. * * Shaders are programs that run on the graphics processing unit (GPU). They * can process many pixels at the same time, making them fast for many * graphics tasks. * * Once the p5.Shader object is created, it can be * used with the shader() function, as in * `shader(myShader)`. A GLSL shader program consists of two parts, a vertex shader * and a fragment shader. The vertex shader affects where 3D geometry is drawn * on the screen and the fragment shader affects color. * * The first parameter, `vertSrc`, sets the vertex shader. It’s a string that * contains the vertex shader program written in GLSL. * * The second parameter, `fragSrc`, sets the fragment shader. It’s a string * that contains the fragment shader program written in GLSL. * * Here is a simple example with a simple vertex shader that applies whatevre * transformations have been set, and a simple fragment shader that ignores * all material settings and just outputs yellow: * * ```js example * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision highp float; * * void main() { * // Set each pixel's RGBA value to yellow. * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); * } * `; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * let shaderProgram = createShader(vertSrc, fragSrc); * * // Compile and apply the p5.Shader object. * shader(shaderProgram); * * // Style the drawing surface. * noStroke(); * * // Add a plane as a drawing surface. * plane(100, 100); * * describe('A yellow square.'); * } * ``` * * Fragment shaders are often the fastest way to dynamically create per-pixel textures. * Here is an example of a fractal being drawn in the fragment shader. It also creates custom * *uniform* variables in the shader, which can be set from your main sketch code. By passing * the time in as a uniform, we can animate the fractal in the shader. * * ```js example * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision highp float; * uniform vec2 p; * uniform float r; * const int numIterations = 500; * varying vec2 vTexCoord; * * void main() { * vec2 c = p + gl_FragCoord.xy * r; * vec2 z = c; * float n = 0.0; * * for (int i = numIterations; i > 0; i--) { * if (z.x * z.x + z.y * z.y > 4.0) { * n = float(i) / float(numIterations); * break; * } * * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; * } * * gl_FragColor = vec4( * 0.5 - cos(n * 17.0) / 2.0, * 0.5 - cos(n * 13.0) / 2.0, * 0.5 - cos(n * 23.0) / 2.0, * 1.0 * ); * } * `; * * let mandelbrot; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * mandelbrot = createShader(vertSrc, fragSrc); * * // Apply the p5.Shader object. * shader(mandelbrot); * * // Set the shader uniform p to an array. * // p is the center point of the Mandelbrot image. * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); * * describe('A fractal image zooms in and out of focus.'); * } * * function draw() { * // Set the shader uniform r to a value that oscillates * // between 0 and 0.005. * // r is the size of the image in Mandelbrot-space. * let radius = 0.005 * (sin(frameCount * 0.01) + 1); * mandelbrot.setUniform('r', radius); * * // Add a plane as a drawing surface. * noStroke(); * plane(100, 100); * } * ``` * * A shader can optionally describe *hooks,* which are functions in GLSL that * users may choose to provide to customize the behavior of the shader using the * `modify()` method of `p5.Shader`. Users can * write their modifications using p5.strands, without needing to learn GLSL. * * These are added by * describing the hooks in a third parameter, `options`, and referencing the hooks in * your `vertSrc` or `fragSrc`. Hooks for the vertex or fragment shader are described under * the `vertex` and `fragment` keys of `options`. Each one is an object. where each key is * the type and name of a hook function, and each value is a string with the * parameter list and default implementation of the hook. For example, to let users * optionally run code at the start of the vertex shader, the options object could * include: * * ```js * { * vertex: { * 'void beforeVertex': '() {}' * } * } * ``` * * Then, in your vertex shader source, you can run a hook by calling a function * with the same name prefixed by `HOOK_`. If you want to check if the default * hook has been replaced, maybe to avoid extra overhead, you can check if the * same name prefixed by `AUGMENTED_HOOK_` has been defined: * * ```glsl * void main() { * // In most cases, just calling the hook is fine: * HOOK_beforeVertex(); * * // Alternatively, for more efficiency: * #ifdef AUGMENTED_HOOK_beforeVertex * HOOK_beforeVertex(); * #endif * * // Add the rest of your shader code here! * } * ``` * * Then, a user of your shader can modify it with p5.strands. Here is what * that looks like when we put everything together: * * ```js example * // A shader with hooks. * let myShader; * * // A shader with modified hooks. * let modifiedShader; * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * attribute vec3 aPosition; * attribute vec2 aTexCoord; * * void main() { * vec4 positionVec4 = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } * `; * * // Create a fragment shader that uses a hook. * let fragSrc = ` * precision highp float; * void main() { * // Let users override the color * gl_FragColor = HOOK_getColor(vec4(1., 0., 0., 1.)); * } * `; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a shader with hooks. By default, this hook returns * // the initial value. * myShader = createShader(vertSrc, fragSrc, { * fragment: { * 'vec4 getColor': '(vec4 color) { return color; }' * } * }); * * // Make a version of the shader with a hook overridden * modifiedShader = myShader.modify(() => { * // Create new uniforms and override the getColor hook * let t = uniformFloat(() => millis() / 1000); * getColor(() => { * return [0, 0.5 + 0.5 * sin(t), 1, 1]; * }); * }); * } * * function draw() { * noStroke(); * * push(); * shader(myShader); * translate(-width/3, 0); * sphere(20); * pop(); * * push(); * shader(modifiedShader); * translate(width/3, 0); * sphere(20); * pop(); * } * ``` * * Note: Only filter shaders can be used in 2D mode. All shaders can be used * in WebGL mode. * * @method createShader * @param {String} vertSrc source code for the vertex shader. * @param {String} fragSrc source code for the fragment shader. * @param {Object} [options] An optional object describing how this shader can * be augmented with hooks. It can include: * @param {Object} [options.vertex] An object describing the available vertex shader hooks. * @param {Object} [options.fragment] An object describing the available frament shader hooks. * @returns {p5.Shader} new shader object created from the * vertex and fragment shaders. */ fn.createShader = function (vertSrc, fragSrc, options) { // p5._validateParameters('createShader', arguments); return new Shader(this._renderer, vertSrc, fragSrc, options); }; /** * Loads a new shader from a file that can be applied to the contents of the canvas with * `filter()`. Pass the resulting shader into `filter()` to apply it. * * Since this function loads data from another file, it returns a `Promise`. * Use it in an `async function setup`, and `await` its result. * * ```js * async function setup() { * createCanvas(50, 50, WEBGL); * let img = await loadImage('assets/bricks.jpg'); * let myFilter = await loadFilterShader('myFilter.js'); * * image(img, -50, -50); * filter(myFilter); * describe('Bricks tinted red'); * } * ``` * * Inside your shader file, you can use p5.strands hooks to change parts of the shader. For * a filter shader, use `filterColor` to change each pixel on the canvas. * * ```js * // myFilter.js * filterColor.begin(); * let result = getTexture( * filterColor.canvasContent, * filterColor.texCoord * ); * // Zero out the green and blue channels, leaving red * result.g = 0; * result.b = 0; * filterColor.set(result); * filterColor.end(); * ``` * * Read the reference for `buildFilterShader`, * the version of `loadFilterShader` that takes in a function instead of a separate file, * for more examples. * * The second parameter, `successCallback`, is optional. If a function is passed, as in * `loadFilterShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. * The return value of `handleData()`, if present, will be used as the final return value of * `loadFilterShader('myShader.js', onLoaded)`. * * @method loadFilterShader * @submodule p5.strands * @param {String} filename path to a p5.strands JavaScript file or a GLSL fragment shader file * @param {Function} [successCallback] callback to be called once the shader is * loaded. Will be passed the * p5.Shader object. * @param {Function} [failureCallback] callback to be called if there is an error * loading the shader. Will be passed the * error event. * @return {Promise} a promise that resolves with a shader object */ fn.loadFilterShader = async function ( fragFilename, successCallback, failureCallback, ) { // p5._validateParameters('loadFilterShader', arguments); try { // Load the fragment shader const fragSrc = await this.loadStrings(fragFilename); const fragString = await fragSrc.join("\n"); // Test if we've loaded GLSL or not by checking for the existence of `void main` let loadedShader; if (/void\s+main/.exec(fragString)) { loadedShader = this.createFilterShader(fragString, true); } else { loadedShader = withGlobalStrands(this, () => this.baseFilterShader().modify(new Function(fragString)), ); } if (successCallback) { loadedShader = successCallback(loadedShader) || loadedShader; } return loadedShader; } catch (err) { if (failureCallback) { failureCallback(err); } else { console.error(err); } } }; /** * Creates a p5.Shader object to be used with the * filter() function. * * The main way to use `buildFilterShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can use `filterColor` with a function * that will be called for each pixel on the image to determine its final color. You can * read the color of the current pixel with `getTexture(canvasContent, coord)`. * See getTexture(). * * ```js example * async function setup() { * createCanvas(50, 50, WEBGL); * let img = await loadImage('assets/bricks.jpg'); * let myFilter = buildFilterShader(tintShader); * * image(img, -50, -50); * filter(myFilter); * describe('Bricks tinted red'); * } * * function tintShader() { * filterColor.begin(); * let result = getTexture( * filterColor.canvasContent, * filterColor.texCoord * ); * // Zero out the green and blue channels, leaving red * result.g = 0; * result.b = 0; * filterColor.set(result); * filterColor.end(); * } * ``` * * You can create *uniforms* if you want to pass data into your filter from the rest of your sketch. * For example, you could pass in the mouse cursor position and use that to control how much * you warp the content. If you create a uniform inside the shader using a function like `uniformFloat()`, with * `uniform` + the type of the data, you can set its value using `setUniform` right before applying the filter. * In the example below, move your mouse across the image to see it update the `warpAmount` uniform: * * ```js example * let img; * let myFilter; * async function setup() { * createCanvas(50, 50, WEBGL); * img = await loadImage('assets/bricks.jpg'); * myFilter = buildFilterShader(warpShader); * describe('Warped bricks'); * } * * function warpShader() { * let warpAmount = uniformFloat(); * filterColor.begin(); * let coord = filterColor.texCoord; * coord.y += sin(coord.x * 10) * warpAmount; * filterColor.set( * getTexture(filterColor.canvasContent, coord) * ); * filterColor.end(); * } * * function draw() { * image(img, -50, -50); * myFilter.setUniform( * 'warpAmount', * map(mouseX, 0, width, 0, 1, true) * ); * filter(myFilter); * } * ``` * * You can also make filters that do not need any content to be drawn first! * There is a lot you can draw just using, for example, the position of the pixel. * `inputs.texCoord` has an `x` and a `y` property, each with a number between 0 and 1. * * ```js example * function setup() { * createCanvas(50, 50, WEBGL); * let myFilter = buildFilterShader(gradient); * describe('A gradient with red, green, yellow, and black'); * filter(myFilter); * } * * function gradient() { * filterColor.begin(); * filterColor.set([filterColor.texCoord.x, filterColor.texCoord.y, 0, 1]); * filterColor.end(); * } * ``` * * ```js example * function setup() { * createCanvas(50, 50, WEBGL); * let myFilter = buildFilterShader(gradient); * describe('A gradient from red to blue'); * filter(myFilter); * } * * function gradient() { * filterColor.begin(); * filterColor.set(mix( * [1, 0, 0, 1], // Red * [0, 0, 1, 1], // Blue * filterColor.texCoord.x // x coordinate, from 0 to 1 * )); * filterColor.end(); * } * ``` * * You can also animate your filters over time by passing the time into the shader with `uniformFloat`. * * ```js example * let myFilter; * function setup() { * createCanvas(50, 50, WEBGL); * myFilter = buildFilterShader(gradient); * describe('A moving, repeating gradient from red to blue'); * } * * function gradient() { * let time = uniformFloat(); * filterColor.begin(); * filterColor.set(mix( * [1, 0, 0, 1], // Red * [0, 0, 1, 1], // Blue * sin(filterColor.texCoord.x*15 + time*0.004)/2+0.5 * )); * filterColor.end(); * } * * function draw() { * myFilter.setUniform('time', millis()); * filter(myFilter); * } * ``` * * Like the `modify()` method on shaders, * advanced users can also fill in `filterColor` using GLSL * instead of JavaScript. * Read the reference entry for `modify()` * for more info. Alternatively, `buildFilterShader()` can also be used like * createShader(), but where you only specify a fragment shader. * * For more info about filters and shaders, see Adam Ferriss' repo of shader examples * or the Introduction to Shaders tutorial. * * @method buildFilterShader * @beta * @submodule p5.strands * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader */ /** * @method buildFilterShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The material shader */ fn.buildFilterShader = function (callback) { return this.baseFilterShader().modify(callback); }; /** * Creates a p5.Shader object to be used with the * filter() function using GLSL. * * Since this method requires you to write your shaders in GLSL, it is most suitable * for advanced use cases. Consider using `buildFilterShader` * first, as a way to create filters in JavaScript using p5.strands. * * `createFilterShader()` works like * createShader() but has a default vertex * shader included. `createFilterShader()` is intended to be used along with * filter() for filtering the contents of a canvas. * A filter shader will be applied to the whole canvas instead of just * p5.Geometry objects. * * The parameter, `fragSrc`, sets the fragment shader. It’s a string that * contains the fragment shader program written in * GLSL. * * The p5.Shader object that's created has some * uniforms that can be set: * - `sampler2D tex0`, which contains the canvas contents as a texture. * - `vec2 canvasSize`, which is the width and height of the canvas, not including pixel density. * - `vec2 texelSize`, which is the size of a physical pixel including pixel density. This is calculated as `1.0 / (width * density)` for the pixel width and `1.0 / (height * density)` for the pixel height. * * The p5.Shader that's created also provides * `varying vec2 vTexCoord`, a coordinate with values between 0 and 1. * `vTexCoord` describes where on the canvas the pixel will be drawn. * * For more info about filters and shaders, see Adam Ferriss' repo of shader examples * or the Introduction to Shaders tutorial. * * @method createFilterShader * @param {String} fragSrc source code for the fragment shader. * @returns {p5.Shader} new shader object created from the fragment shader. * * @example * function setup() { * let fragSrc = `precision highp float; * void main() { * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); * }`; * * createCanvas(100, 100, WEBGL); * let s = createFilterShader(fragSrc); * filter(s); * describe('a yellow canvas'); * } * * @example * let img, s; * async function setup() { * img = await loadImage('assets/bricks.jpg'); * let fragSrc = `precision highp float; * * // x,y coordinates, given from the vertex shader * varying vec2 vTexCoord; * * // the canvas contents, given from filter() * uniform sampler2D tex0; * // other useful information from the canvas * uniform vec2 texelSize; * uniform vec2 canvasSize; * // a custom variable from this sketch * uniform float darkness; * * void main() { * // get the color at current pixel * vec4 color = texture2D(tex0, vTexCoord); * // set the output color * color.b = 1.0; * color *= darkness; * gl_FragColor = vec4(color.rgb, 1.0); * }`; * * createCanvas(100, 100, WEBGL); * s = createFilterShader(fragSrc); * } * * function draw() { * image(img, -50, -50); * s.setUniform('darkness', 0.5); * filter(s); * describe('a image of bricks tinted dark blue'); * } */ fn.createFilterShader = function (fragSrc, skipContextCheck = false) { // p5._validateParameters('buildFilterShader', arguments); let defaultVertV1 = ` uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; attribute vec3 aPosition; // texcoords only come from p5 to vertex shader // so pass texcoords on to the fragment shader in a varying variable attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { // transferring texcoords for the frag shader vTexCoord = aTexCoord; // copy position with a fourth coordinate for projection (1.0 is normal) vec4 positionVec4 = vec4(aPosition, 1.0); // project to 3D space gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; } `; let defaultVertV2 = `#version 300 es uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; in vec3 aPosition; in vec2 aTexCoord; out vec2 vTexCoord; void main() { // transferring texcoords for the frag shader vTexCoord = aTexCoord; // copy position with a fourth coordinate for projection (1.0 is normal) vec4 positionVec4 = vec4(aPosition, 1.0); // project to 3D space gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; } `; let vertSrc = fragSrc.includes("#version 300 es") ? defaultVertV2 : defaultVertV1; const shader = new Shader(this._renderer, vertSrc, fragSrc); if (!skipContextCheck) { if (this._renderer.GL) { shader.ensureCompiledOnContext(this._renderer); } else { shader.ensureCompiledOnContext(this); } } return shader; }; /** * Sets the p5.Shader object to apply while drawing. * * Shaders are programs that run on the graphics processing unit (GPU). They * can process many pixels or vertices at the same time, making them fast for * many graphics tasks. * * You can make new shaders using p5.strands with the * `buildMaterialShader`, * `buildColorShader`, and * `buildNormalShader` functions. You can also use * `buildFilterShader` alongside * `filter`, and * `buildStrokeShader` alongside * `stroke`. * * The parameter, `s`, is the p5.Shader object to * apply. For example, calling `shader(myShader)` applies `myShader` to * process each pixel on the canvas. This only changes the fill (the inner part of shapes), * but does not affect the outlines (strokes) or any images drawn using the `image()` function. * The source code from a p5.Shader object's * fragment and vertex shaders will be compiled the first time it's passed to * `shader()`. * * Calling resetShader() restores a sketch’s * default shaders. * * Note: Shaders can only be used in WebGL mode. * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * noStroke(); * describe('A square with dynamically changing colors on a beige background.'); * } * * function material() { * let time = uniformFloat(); * finalColor.begin(); * let r = 0.2 + 0.5 * abs(sin(time + 0)); * let g = 0.2 + 0.5 * abs(sin(time + 1)); * let b = 0.2 + 0.5 * abs(sin(time + 2)); * finalColor.set([r, g, b, 1]); * finalColor.end(); * } * * function draw() { * background(245, 245, 220); * myShader.setUniform('time', millis() / 1000); * shader(myShader); * * rectMode(CENTER); * rect(0, 0, 50, 50); * } * ``` * * For advanced usage, shaders can be written in a language called * GLSL. * p5.Shader objects can be created in this way using the * createShader() and * loadShader() functions. * * ```js * let fillShader; * * let vertSrc = ` * precision highp float; * attribute vec3 aPosition; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * varying vec3 vPosition; * * void main() { * vPosition = aPosition; * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); * } * `; * * let fragSrc = ` * precision highp float; * uniform vec3 uLightDir; * varying vec3 vPosition; * * void main() { * vec3 lightDir = normalize(uLightDir); * float brightness = dot(lightDir, normalize(vPosition)); * brightness = clamp(brightness, 0.4, 1.0); * vec3 color = vec3(0.3, 0.5, 1.0); * color = color * brightness * 3.0; * gl_FragColor = vec4(color, 1.0); * } * `; * * function setup() { * createCanvas(200, 200, WEBGL); * fillShader = createShader(vertSrc, fragSrc); * noStroke(); * describe('A rotating torus with simulated directional lighting.'); * } * * function draw() { * background(20, 20, 40); * let lightDir = [0.5, 0.5, 1.0]; * fillShader.setUniform('uLightDir', lightDir); * shader(fillShader); * rotateY(frameCount * 0.02); * rotateX(frameCount * 0.02); * torus(25, 10, 30, 30); * } * ``` * * ```js example * let fillShader; * * let vertSrc = ` * precision highp float; * attribute vec3 aPosition; * uniform mat4 uProjectionMatrix; * uniform mat4 uModelViewMatrix; * varying vec3 vPosition; * void main() { * vPosition = aPosition; * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); * } * `; * * let fragSrc = ` * precision highp float; * uniform vec3 uLightPos; * uniform vec3 uFillColor; * varying vec3 vPosition; * void main() { * float brightness = dot(normalize(uLightPos), normalize(vPosition)); * brightness = clamp(brightness, 0.0, 1.0); * vec3 color = uFillColor * brightness; * gl_FragColor = vec4(color, 1.0); * } * `; * * function setup() { * createCanvas(200, 200, WEBGL); * fillShader = createShader(vertSrc, fragSrc); * shader(fillShader); * noStroke(); * describe('A square affected by both fill color and lighting, with lights controlled by mouse.'); * } * * function draw() { * let lightPos = [(mouseX - width / 2) / width, * (mouseY - height / 2) / height, 1.0]; * fillShader.setUniform('uLightPos', lightPos); * let fillColor = [map(mouseX, 0, width, 0, 1), * map(mouseY, 0, height, 0, 1), 0.5]; * fillShader.setUniform('uFillColor', fillColor); * plane(width, height); * } * ``` * *
*

* * If you want to apply shaders to strokes or images, use the following methods: * - strokeShader() : Applies a shader to the stroke (outline) of shapes, allowing independent control over the stroke rendering using shaders. * - imageShader() : Applies a shader to images or textures, controlling how the shader modifies their appearance during rendering. * *

*
* * * @method shader * @chainable * @param {p5.Shader} s p5.Shader object * to apply. * */ fn.shader = function (s) { this._assert3d('shader'); // p5._validateParameters('shader', arguments); this._renderer.shader(s); return this; }; /** * Sets the p5.Shader object to apply for strokes. * * This method applies the given shader to strokes, allowing customization of * how lines and outlines are drawn in 3D space. The shader will be used for * strokes until resetShader() is called or another * strokeShader is applied. * * The shader will be used for: * - Strokes only, regardless of whether the uniform `uStrokeWeight` is present. * * To further customize its behavior, refer to the various hooks provided by * the baseStrokeShader() method, which allow * control over stroke weight, vertex positions, colors, and more. * * @method strokeShader * @chainable * @param {p5.Shader} s p5.Shader object * to apply for strokes. * * * @example * let animatedStrokeShader; * * let vertSrc = ` * precision mediump int; * * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * uniform float uStrokeWeight; * * uniform bool uUseLineColor; * uniform vec4 uMaterialColor; * * uniform vec4 uViewport; * uniform int uPerspective; * uniform int uStrokeJoin; * * attribute vec4 aPosition; * attribute vec3 aTangentIn; * attribute vec3 aTangentOut; * attribute float aSide; * attribute vec4 aVertexColor; * * void main() { * vec4 posp = uModelViewMatrix * aPosition; * vec4 posqIn = uModelViewMatrix * (aPosition + vec4(aTangentIn, 0)); * vec4 posqOut = uModelViewMatrix * (aPosition + vec4(aTangentOut, 0)); * * float facingCamera = pow( * abs(normalize(posqIn-posp).z), * 0.25 * ); * * float scale = mix(1., 0.995, facingCamera); * * posp.xyz = posp.xyz * scale; * posqIn.xyz = posqIn.xyz * scale; * posqOut.xyz = posqOut.xyz * scale; * * vec4 p = uProjectionMatrix * posp; * vec4 qIn = uProjectionMatrix * posqIn; * vec4 qOut = uProjectionMatrix * posqOut; * * vec2 tangentIn = normalize((qIn.xy*p.w - p.xy*qIn.w) * uViewport.zw); * vec2 tangentOut = normalize((qOut.xy*p.w - p.xy*qOut.w) * uViewport.zw); * * vec2 curPerspScale; * if(uPerspective == 1) { * curPerspScale = (uProjectionMatrix * vec4(1, sign(uProjectionMatrix[1][1]), 0, 0)).xy; * } else { * curPerspScale = p.w / (0.5 * uViewport.zw); * } * * vec2 offset; * vec2 tangent = aTangentIn == vec3(0.) ? tangentOut : tangentIn; * vec2 normal = vec2(-tangent.y, tangent.x); * float normalOffset = sign(aSide); * float tangentOffset = abs(aSide) - 1.; * offset = (normal * normalOffset + tangent * tangentOffset) * * uStrokeWeight * 0.5; * * gl_Position.xy = p.xy + offset.xy * curPerspScale; * gl_Position.zw = p.zw; * } * `; * * let fragSrc = ` * precision mediump float; * uniform float uTime; * * void main() { * float wave = sin(gl_FragCoord.x * 0.1 + uTime) * 0.5 + 0.5; * gl_FragColor = vec4(wave, 0.5, 1.0, 1.0); // Animated color based on time * } * `; * * function setup() { * createCanvas(200, 200, WEBGL); * animatedStrokeShader = createShader(vertSrc, fragSrc); * strokeShader(animatedStrokeShader); * strokeWeight(4); * * describe('A hollow cube rotating continuously with its stroke colors changing dynamically over time against a static gray background.'); * } * * function draw() { * animatedStrokeShader.setUniform('uTime', millis() / 1000.0); * background(250); * rotateY(frameCount * 0.02); * noFill(); * orbitControl(); * box(50); * } * * @example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseStrokeShader().modify({ * 'float random': `(vec2 p) { * vec3 p3 = fract(vec3(p.xyx) * .1471); * p3 += dot(p3, p3.yzx + 32.33); * return fract((p3.x + p3.y) * p3.z); * }`, * 'Inputs getPixelInputs': `(Inputs inputs) { * // Modify alpha with dithering effect * float a = inputs.color.a; * inputs.color.a = 1.0; * inputs.color *= random(inputs.position.xy) > a ? 0.0 : 1.0; * return inputs; * }` * }); * } * * function draw() { * background(255); * strokeShader(myShader); * strokeWeight(12); * beginShape(); * for (let i = 0; i <= 50; i++) { * stroke( * map(i, 0, 50, 150, 255), * 100 + 155 * sin(i / 5), * 255 * map(i, 0, 50, 1, 0) * ); * vertex( * map(i, 0, 50, 1, -1) * width / 3, * 50 * cos(i / 10 + frameCount / 80) * ); * } * endShape(); * } */ fn.strokeShader = function (s) { this._assert3d("strokeShader"); // p5._validateParameters('strokeShader', arguments); this._renderer.strokeShader(s); return this; }; /** * Sets the p5.Shader object to apply for images. * * This method allows the user to apply a custom shader to images, enabling * advanced visual effects such as pixel manipulation, color adjustments, * or dynamic behavior. The shader will be applied to the image drawn using * the image() function. * * The shader will be used exclusively for: * - `image()` calls, applying only when drawing 2D images. * - This shader will NOT apply to images used in texture() or other 3D contexts. * Any attempts to use the imageShader in these cases will be ignored. * * @method imageShader * @chainable * @param {p5.Shader} s p5.Shader object * to apply for images. * * @example * let img; * let imgShader; * * async function setup() { * img = await loadImage('assets/outdoor_image.jpg'); * * createCanvas(200, 200, WEBGL); * noStroke(); * * imgShader = createShader(` * precision mediump float; * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * void main() { * vTexCoord = aTexCoord; * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); * } * `, ` * precision mediump float; * varying vec2 vTexCoord; * uniform sampler2D uTexture; * uniform vec2 uMousePos; * * void main() { * vec4 texColor = texture2D(uTexture, vTexCoord); * // Adjust the color based on mouse position * float r = uMousePos.x * texColor.r; * float g = uMousePos.y * texColor.g; * gl_FragColor = vec4(r, g, texColor.b, texColor.a); * } * `); * * describe( * 'An image on a gray background where the colors change based on the mouse position.' * ); * } * * function draw() { * background(220); * * imageShader(imgShader); * * // Map the mouse position to a range between 0 and 1 * let mousePosX = map(mouseX, 0, width, 0, 1); * let mousePosY = map(mouseY, 0, height, 0, 1); * * // Pass the mouse position to the shader as a uniform * imgShader.setUniform('uMousePos', [mousePosX, mousePosY]); * * // Bind the image texture to the shader * imgShader.setUniform('uTexture', img); * * image(img, -width / 2, -height / 2, width, height); * } * * * @example * let img; * let imgShader; * * async function setup() { * img = await loadImage('assets/outdoor_image.jpg'); * * createCanvas(200, 200, WEBGL); * noStroke(); * * imgShader = createShader(` * precision mediump float; * attribute vec3 aPosition; * attribute vec2 aTexCoord; * varying vec2 vTexCoord; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * * void main() { * vTexCoord = aTexCoord; * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); * } * `, ` * precision mediump float; * varying vec2 vTexCoord; * uniform sampler2D uTexture; * uniform vec2 uMousePos; * * void main() { * // Distance from the current pixel to the mouse * float distFromMouse = distance(vTexCoord, uMousePos); * * // Adjust pixelation based on distance (closer = more detail, farther = blockier) * float pixelSize = mix(0.002, 0.05, distFromMouse); * vec2 pixelatedCoord = vec2(floor(vTexCoord.x / pixelSize) * pixelSize, * floor(vTexCoord.y / pixelSize) * pixelSize); * * vec4 texColor = texture2D(uTexture, pixelatedCoord); * gl_FragColor = texColor; * } * `); * * describe('A static image with a grid-like, pixelated effect created by the shader. Each cell in the grid alternates visibility, producing a dithered visual effect.'); * } * * function draw() { * background(220); * imageShader(imgShader); * * let mousePosX = map(mouseX, 0, width, 0, 1); * let mousePosY = map(mouseY, 0, height, 0, 1); * * imgShader.setUniform('uMousePos', [mousePosX, mousePosY]); * imgShader.setUniform('uTexture', img); * image(img, -width / 2, -height / 2, width, height); * } */ fn.imageShader = function (s) { this._assert3d("imageShader"); // p5._validateParameters('imageShader', arguments); this._renderer.imageShader(s); return this; }; /** * Create a new shader that can change how fills are drawn. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. * * The main way to use `buildMaterialShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. * - `pixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. * - `combineColors`: Control how the ambient, diffuse, and specular components of lighting are combined into a single color on the surface of a shape. Your function gets run on every pixel. * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * * One thing you can do with a material shader is animate the positions of vertices * over time: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * let time = uniformFloat(); * worldInputs.begin(); * worldInputs.position.y += * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); * worldInputs.end(); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); * sphere(50); * } * ``` * * There are also many uses in updating values per pixel. This can be a good * way to give your sketch texture and detail. For example, instead of having a single * shininess or metalness value for a whole shape, you could vary it in different spots on its surface: * * ```js example * let myShader; * let environment; * * async function setup() { * environment = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * pixelInputs.begin(); * let factor = sin( * TWO_PI * (pixelInputs.texCoord.x + pixelInputs.texCoord.y) * ); * pixelInputs.shininess = mix(1, 100, factor); * pixelInputs.metalness = factor; * pixelInputs.end(); * } * * function draw() { * panorama(environment); * ambientLight(100); * imageLight(environment); * rotateY(millis() * 0.001); * shader(myShader); * noStroke(); * fill(255); * specularMaterial(150); * sphere(50); * } * ``` * * A technique seen often in games called *bump mapping* is to vary the * *normal*, which is the orientation of the surface, per pixel to create texture * rather than using many tightly packed vertices. Sometimes this can come from * bump images, but it can also be done generatively with math. * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * pixelInputs.begin(); * pixelInputs.normal.x += 0.2 * sin( * sin(TWO_PI * dot(pixelInputs.texCoord.yx, vec2(10, 25))) * ); * pixelInputs.normal.y += 0.2 * sin( * sin(TWO_PI * dot(pixelInputs.texCoord, vec2(10, 25))) * ); * pixelInputs.normal = normalize(pixelInputs.normal); * pixelInputs.end(); * } * * function draw() { * background(255); * shader(myShader); * ambientLight(150); * pointLight( * 255, 255, 255, * 100*cos(frameCount*0.04), -50, 100*sin(frameCount*0.04) * ); * noStroke(); * fill('red'); * shininess(200); * specularMaterial(255); * sphere(50); * } * ``` * * You can also update the final color directly instead of modifying * lighting settings. Sometimes in photographs, a light source is placed * behind the subject to create *rim lighting,* where the edges of the * subject are lit up. This can be simulated by adding white to the final * color on parts of the shape that are facing away from the camera. * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * let myNormal = sharedVec3(); * * pixelInputs.begin(); * myNormal = pixelInputs.normal; * pixelInputs.end(); * * finalColor.begin(); * finalColor.set(mix( * [1, 1, 1, 1], * finalColor.color, * abs(dot(myNormal, [0, 0, 1])) * )); * finalColor.end(); * } * * function draw() { * background(255); * rotateY(millis() * 0.001); * shader(myShader); * lights(); * noStroke(); * fill('red'); * torus(30); * } * ``` * * Like the `modify()` method on shaders, * advanced users can also fill in hooks using GLSL * instead of JavaScript. * Read the reference entry for `modify()` * for more info. * * @method buildMaterialShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader. */ /** * @method buildMaterialShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The material shader. */ fn.buildMaterialShader = function (cb) { return this.baseMaterialShader().modify(cb); }; /** * Loads a new shader from a file that can change how fills are drawn. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. * * Since this function loads data from another file, it returns a `Promise`. * Use it in an `async function setup`, and `await` its result. * * ```js * let myShader; * async function setup() { * createCanvas(200, 200, WEBGL); * myShader = await loadMaterialShader('myMaterial.js'); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); * sphere(50); * } * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For * example, you might use the `worldInputs` hook to change each vertex, or you * might use the `pixelInputs` hook to change each pixel on the surface of a shape. * * ```js * // myMaterial.js * let time = uniformFloat(); * worldInputs.begin(); * worldInputs.position.y += * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); * worldInputs.end(); * ``` * * Read the reference for `buildMaterialShader`, * the version of `loadMaterialShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * * The second parameter, `successCallback`, is optional. If a function is passed, as in * `loadMaterialShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. * The return value of `handleData()`, if present, will be used as the final return value of * `loadMaterialShader('myShader.js', onLoaded)`. * * @method loadMaterialShader * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. * @param {Function} [onFailure] A callback function to run when loading fails. * @returns {Promise} The material shader. */ fn.loadMaterialShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); let shader = withGlobalStrands(this, () => this.buildMaterialShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; } return shader; } catch (e) { console.error(e); if (onFail) { onFail(e); } } }; /** * Returns the default shader used for fills when lights or textures are used. * * Calling `buildMaterialShader(shaderFunction)` * is equivalent to calling `baseMaterialShader().modify(shaderFunction)`. * * Read the `buildMaterialShader` reference or * call `baseMaterialShader().inspectHooks()` for more information on what you can do with * the base material shader. * * @method baseMaterialShader * @submodule p5.strands * @beta * @returns {p5.Shader} The base material shader. */ fn.baseMaterialShader = function () { this._assert3d("baseMaterialShader"); return this._renderer.baseMaterialShader(); }; /** * Returns the base shader used for filters. * * Calling `buildFilterShader(shaderFunction)` * is equivalent to calling `baseFilterShader().modify(shaderFunction)`. * * Read the `buildFilterShader` reference or * call `baseFilterShader().inspectHooks()` for more information on what you can do with * the base filter shader. * * @method baseFilterShader * @submodule p5.strands * @beta * @returns {p5.Shader} The base filter shader. */ fn.baseFilterShader = function () { return (this._renderer.filterRenderer || this._renderer).baseFilterShader(); }; /** * Create a new shader that can change how fills are drawn, based on the material used * when `normalMaterial()` is active. Pass the resulting * shader into the `shader()` function to apply it to any fills * you draw. * * The main way to use `buildNormalShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * * One thing you may want to do is update the position of all the vertices in an object over time: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildNormalShader(material); * } * * function material() { * let time = uniformFloat(); * worldInputs.begin(); * worldInputs.position.y += * 20. * sin(time * 0.001 + worldInputs.position.x * 0.05); * worldInputs.end(); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('time', millis()); * noStroke(); * sphere(50); * } * ``` * * You may also want to change the colors used. By default, the x, y, and z values of the orientation * of the surface are mapped directly to red, green, and blue. But you can pick different colors: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildNormalShader(material); * } * * function material() { * cameraInputs.begin(); * cameraInputs.normal = abs(cameraInputs.normal); * cameraInputs.end(); * * finalColor.begin(); * // Map the r, g, and b values of the old normal to new colors * // instead of just red, green, and blue: * let newColor = * finalColor.color.r * [89, 240, 232] / 255 + * finalColor.color.g * [240, 237, 89] / 255 + * finalColor.color.b * [205, 55, 222] / 255; * newColor = newColor / (finalColor.color.r + finalColor.color.g + finalColor.color.b); * finalColor.set([newColor.r, newColor.g, newColor.b, finalColor.color.a]); * finalColor.end(); * } * * function draw() { * background(255); * shader(myShader); * noStroke(); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.015); * box(100); * } * ``` * * Like the `modify()` method on shaders, * advanced users can also fill in hooks using GLSL * instead of JavaScript. * Read the reference entry for `modify()` * for more info. * * @method buildNormalShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The normal shader. */ /** * @method buildNormalShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The normal shader. */ fn.buildNormalShader = function (cb) { return this.baseNormalShader().modify(cb); }; /** * Loads a new shader from a file that can change how fills are drawn, based on the material used * when `normalMaterial()` is active. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. * * Since this function loads data from another file, it returns a `Promise`. * Use it in an `async function setup`, and `await` its result. * * ```js * let myShader; * async function setup() { * createCanvas(200, 200, WEBGL); * myShader = await loadNormalShader('myMaterial.js'); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); * sphere(50); * } * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For * example, you might use the `worldInputs` hook to change each vertex, or you * might use the `finalColor` hook to change the color of each pixel on the surface of a shape. * * ```js * // myMaterial.js * let time = uniformFloat(); * worldInputs.begin(); * worldInputs.position.y += * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); * worldInputs.end(); * ``` * * Read the reference for `buildNormalShader`, * the version of `loadNormalShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * * The second parameter, `successCallback`, is optional. If a function is passed, as in * `loadNormalShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. * The return value of `handleData()`, if present, will be used as the final return value of * `loadNormalShader('myShader.js', onLoaded)`. * * @method loadNormalShader * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. * @param {Function} [onFailure] A callback function to run when loading fails. * @returns {Promise} The normal shader. */ fn.loadNormalShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); let shader = this.withGlobalStrands(this, () => this.buildNormalShader(cb), ); if (onSuccess) { shader = onSuccess(shader) || shader; } return shader; } catch (e) { console.error(e); if (onFail) { onFail(e); } } }; /** * Returns the default shader used for fills when * `normalMaterial()` is activated. * * Calling `buildNormalShader(shaderFunction)` * is equivalent to calling `baseNormalShader().modify(shaderFunction)`. * * Read the `buildNormalShader` reference or * call `baseNormalShader().inspectHooks()` for more information on what you can do with * the base normal shader. * * @method baseNormalShader * @submodule p5.strands * @beta * @returns {p5.Shader} The base material shader. */ fn.baseNormalShader = function () { this._assert3d("baseNormalShader"); return this._renderer.baseNormalShader(); }; /** * Create a new shader that can change how fills are drawn, based on the default shader * used when no lights or textures are applied. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. * * The main way to use `buildColorShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * * One thing you might want to do is modify the position of every vertex over time: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildColorShader(material); * } * * function material() { * let time = uniformFloat(); * worldInputs.begin(); * worldInputs.position.y += * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); * worldInputs.end(); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('time', millis()); * noStroke(); * fill('red'); * circle(0, 0, 50); * } * ``` * * Like the `modify()` method on shaders, * advanced users can also fill in hooks using GLSL * instead of JavaScript. * Read the reference entry for `modify()` * for more info. * * @method buildColorShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The color shader. */ /** * @method buildColorShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The color shader. */ fn.buildColorShader = function (cb) { return this.baseColorShader().modify(cb); }; /** * Loads a new shader from a file that can change how fills are drawn, based on the material used * when no lights or textures are active. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. * * Since this function loads data from another file, it returns a `Promise`. * Use it in an `async function setup`, and `await` its result. * * ```js * let myShader; * async function setup() { * createCanvas(200, 200, WEBGL); * myShader = await loadColorShader('myMaterial.js'); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); * circle(0, 0, 50); * } * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For * example, you might use the `worldInputs` hook to change each vertex, or you * might use the `finalColor` hook to change the color of each pixel on the surface of a shape. * * ```js * // myMaterial.js * let time = uniformFloat(); * worldInputs.begin(); * worldInputs.position.y += * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); * worldInputs.end(); * ``` * * Read the reference for `buildColorShader`, * the version of `loadColorShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * * The second parameter, `successCallback`, is optional. If a function is passed, as in * `loadColorShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. * The return value of `handleData()`, if present, will be used as the final return value of * `loadColorShader('myShader.js', onLoaded)`. * * @method loadColorShader * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. * @param {Function} [onFailure] A callback function to run when loading fails. * @returns {Promise} The color shader. */ fn.loadColorShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); let shader = withGlobalStrands(this, () => this.buildColorShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; } return shader; } catch (e) { console.error(e); if (onFail) { onFail(e); } } }; /** * Returns the default shader used for fills when no lights or textures are activate. * * Calling `buildColorShader(shaderFunction)` * is equivalent to calling `baseColorShader().modify(shaderFunction)`. * * Read the `buildColorShader` reference or * call `baseColorShader().inspectHooks()` for more information on what you can do with * the base color shader. * * @method baseColorShader * @submodule p5.strands * @beta * @returns {p5.Shader} The base color shader. */ fn.baseColorShader = function () { this._assert3d("baseColorShader"); return this._renderer.baseColorShader(); }; /** * Create a new shader that can change how strokes are drawn, based on the default * shader used for strokes. Pass the resulting shader into the * `strokeShader()` function to apply it to any * strokes you draw. * * The main way to use `buildStrokeShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. * - `pixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * * One thing you might want to do is update the color of a stroke per pixel. Here, it is being used * to create a soft texture: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildStrokeShader(material); * } * * function material() { * pixelInputs.begin(); * let opacity = 1 - smoothstep( * 0, * 15, * length(pixelInputs.position - pixelInputs.center) * ); * pixelInputs.color.a *= opacity; * pixelInputs.end(); * } * * function draw() { * background(255); * strokeShader(myShader); * strokeWeight(30); * line( * -width/3, * sin(millis()*0.001) * height/4, * width/3, * sin(millis()*0.001 + 1) * height/4 * ); * } * ``` * * Rather than using opacity, we could use a form of *dithering* to get a different * texture. This involves using only fully opaque or transparent pixels. Here, we * randomly choose which pixels to be transparent: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildStrokeShader(material); * } * * function material() { * pixelInputs.begin(); * // Replace alpha in the color with dithering by * // randomly setting pixel colors to 0 based on opacity * let a = 1; * if (noise(pixelInputs.position.xy) > pixelInputs.color.a) { * a = 0; * } * pixelInputs.color.a = a; * pixelInputs.end(); * } * * function draw() { * background(255); * strokeShader(myShader); * strokeWeight(10); * beginShape(); * for (let i = 0; i <= 50; i++) { * stroke( * 0, * 255 * * map(i, 0, 20, 0, 1, true) * * map(i, 30, 50, 1, 0, true) * ); * vertex( * map(i, 0, 50, -1, 1) * width/3, * 50 * sin(i/10 + frameCount/100) * ); * } * endShape(); * } * ``` * * You might also want to update some properties per vertex, such as the stroke * thickness. This lets you create a more varied line: * * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildStrokeShader(material); * } * * function material() { * let time = uniformFloat(); * worldInputs.begin(); * // Add a somewhat random offset to the weight * // that varies based on position and time * let scale = 0.5 + noise( * worldInputs.position.x * 0.01, * worldInputs.position.y * 0.01, * time * 0.0005 * ); * worldInputs.weight *= scale; * worldInputs.end(); * } * * function draw() { * background(255); * strokeShader(myShader); * myShader.setUniform('time', millis()); * strokeWeight(10); * beginShape(); * for (let i = 0; i <= 50; i++) { * let r = map(i, 0, 50, 0, width/3); * let x = r*cos(i*0.2); * let y = r*sin(i*0.2); * vertex(x, y); * } * endShape(); * } * ``` * * Like the `modify()` method on shaders, * advanced users can also fill in hooks using GLSL * instead of JavaScript. * Read the reference entry for `modify()` * for more info. * * @method buildStrokeShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The stroke shader. */ /** * @method buildStrokeShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The stroke shader. */ fn.buildStrokeShader = function (cb) { return this.baseStrokeShader().modify(cb); }; /** * Loads a new shader from a file that can change how strokes are drawn. Pass the resulting * shader into the `strokeShader()` function to apply it * to any strokes you draw. * * Since this function loads data from another file, it returns a `Promise`. * Use it in an `async function setup`, and `await` its result. * * ```js * let myShader; * async function setup() { * createCanvas(200, 200, WEBGL); * myShader = await loadStrokeShader('myMaterial.js'); * } * * function draw() { * background(255); * strokeShader(myShader); * strokeWeight(30); * line( * -width/3, * sin(millis()*0.001) * height/4, * width/3, * sin(millis()*0.001 + 1) * height/4 * ); * } * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For * example, you might use the `worldInputs` hook to change each vertex, or you * might use the `pixelInputs` hook to change each pixel on the surface of a stroke. * * ```js * // myMaterial.js * pixelInputs.begin(); * let opacity = 1 - smoothstep( * 0, * 15, * length(pixelInputs.position - pixelInputs.center) * ); * pixelInputs.color.a *= opacity; * pixelInputs.end(); * ``` * * Read the reference for `buildStrokeShader`, * the version of `loadStrokeShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * * The second parameter, `successCallback`, is optional. If a function is passed, as in * `loadStrokeShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. * The return value of `handleData()`, if present, will be used as the final return value of * `loadStrokeShader('myShader.js', onLoaded)`. * * @method loadStrokeShader * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. * @param {Function} [onFailure] A callback function to run when loading fails. * @returns {Promise} The stroke shader. */ fn.loadStrokeShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); let shader = withGlobalStrands(this, () => this.buildStrokeShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; } return shader; } catch (e) { console.error(e); if (onFail) { onFail(e); } } }; /** * Returns the default shader used for strokes. * * Calling `buildStrokeShader(shaderFunction)` * is equivalent to calling `baseStrokeShader().modify(shaderFunction)`. * * Read the `buildStrokeShader` reference or * call `baseStrokeShader().inspectHooks()` for more information on what you can do with * the base material shader. * * @method baseStrokeShader * @submodule p5.strands * @beta * @returns {p5.Shader} The base material shader. */ fn.baseStrokeShader = function () { this._assert3d("baseStrokeShader"); return this._renderer.baseStrokeShader(); }; /** * Restores the default shaders. * * `resetShader()` deactivates any shaders previously applied by * shader(), strokeShader(), * or imageShader(). * * Note: Shaders can only be used in WebGL mode. * * @method resetShader * @chainable * * @example * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * attribute vec3 aPosition; * attribute vec2 aTexCoord; * uniform mat4 uProjectionMatrix; * uniform mat4 uModelViewMatrix; * varying vec2 vTexCoord; * * void main() { * vTexCoord = aTexCoord; * vec4 position = vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * uModelViewMatrix * position; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision mediump float; * varying vec2 vTexCoord; * * void main() { * vec2 uv = vTexCoord; * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); * gl_FragColor = vec4(color, 1.0); * } * `; * * let myShader; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Shader object. * myShader = createShader(vertSrc, fragSrc); * * describe( * 'Two rotating cubes on a gray background. The left one has a blue-purple gradient on each face. The right one is red.' * ); * } * * function draw() { * background(200); * * // Draw a box using the p5.Shader. * // shader() sets the active shader to myShader. * shader(myShader); * push(); * translate(-25, 0, 0); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * box(width / 4); * pop(); * * // Draw a box using the default fill shader. * // resetShader() restores the default fill shader. * resetShader(); * fill(255, 0, 0); * push(); * translate(25, 0, 0); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * box(width / 4); * pop(); * } */ fn.resetShader = function () { this._renderer.resetShader(); return this; }; /** * Sets the texture that will be used on shapes. * * A texture is like a skin that wraps around a shape. `texture()` works with * built-in shapes, such as square() and * sphere(), and custom shapes created with * functions such as buildGeometry(). To * texture a geometry created with beginShape(), * uv coordinates must be passed to each * vertex() call. * * The parameter, `tex`, is the texture to apply. `texture()` can use a range * of sources including images, videos, and offscreen renderers such as * p5.Graphics and * p5.Framebuffer objects. * * To texture a geometry created with beginShape(), * you will need to specify uv coordinates in vertex(). * * Note: `texture()` can only be used in WebGL mode. * * @method texture * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex media to use as the texture. * @chainable * * @example * let img; * * async function setup() { * // Load an image and create a p5.Image object. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100, WEBGL); * * describe('A spinning cube with an image of a ceiling on each face.'); * } * * function draw() { * background(0); * * // Rotate around the x-, y-, and z-axes. * rotateZ(frameCount * 0.01); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * * // Apply the image as a texture. * texture(img); * * // Draw the box. * box(50); * } * * @example * let pg; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Graphics object. * pg = createGraphics(100, 100); * * // Draw a circle to the p5.Graphics object. * pg.background(200); * pg.circle(50, 50, 30); * * describe('A spinning cube with circle at the center of each face.'); * } * * function draw() { * background(0); * * // Rotate around the x-, y-, and z-axes. * rotateZ(frameCount * 0.01); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * * // Apply the p5.Graphics object as a texture. * texture(pg); * * // Draw the box. * box(50); * } * * @example * let vid; * * function setup() { * // Load a video and create a p5.MediaElement object. * vid = createVideo('assets/fingers.mov'); * * createCanvas(100, 100, WEBGL); * * // Hide the video. * vid.hide(); * * // Set the video to loop. * vid.loop(); * * describe('A rectangle with video as texture'); * } * * function draw() { * background(0); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Apply the video as a texture. * texture(vid); * * // Draw the rectangle. * rect(-40, -40, 80, 80); * } * * @example * let vid; * * function setup() { * // Load a video and create a p5.MediaElement object. * vid = createVideo('assets/fingers.mov'); * * createCanvas(100, 100, WEBGL); * * // Hide the video. * vid.hide(); * * // Set the video to loop. * vid.loop(); * * describe('A rectangle with video as texture'); * } * * function draw() { * background(0); * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); * * // Set the texture mode. * textureMode(NORMAL); * * // Apply the video as a texture. * texture(vid); * * // Draw a custom shape using uv coordinates. * beginShape(); * vertex(-40, -40, 0, 0); * vertex(40, -40, 1, 0); * vertex(40, 40, 1, 1); * vertex(-40, 40, 0, 1); * endShape(); * } */ fn.texture = function (tex) { this._assert3d("texture"); // p5._validateParameters('texture', arguments); // NOTE: make generic or remove need for if (tex.gifProperties) { tex._animateGif(this); } this._renderer.texture(tex); return this; }; /** * Changes the coordinate system used for textures when they’re applied to * custom shapes. * * In order for texture() to work, a shape needs a * way to map the points on its surface to the pixels in an image. Built-in * shapes such as rect() and * box() already have these texture mappings based on * their vertices. Custom shapes created with * vertex() require texture mappings to be passed as * uv coordinates. * * Each call to vertex() must include 5 arguments, * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)` * to the pixel at coordinates `(u, v)` within an image. For example, the * corners of a rectangular image are mapped to the corners of a rectangle by default: * * ```js * // Apply the image as a texture. * texture(img); * * // Draw the rectangle. * rect(0, 0, 30, 50); * ``` * * If the image in the code snippet above has dimensions of 300 x 500 pixels, * the same result could be achieved as follows: * * ```js * // Apply the image as a texture. * texture(img); * * // Draw the rectangle. * beginShape(); * * // Top-left. * // u: 0, v: 0 * vertex(0, 0, 0, 0, 0); * * // Top-right. * // u: 300, v: 0 * vertex(30, 0, 0, 300, 0); * * // Bottom-right. * // u: 300, v: 500 * vertex(30, 50, 0, 300, 500); * * // Bottom-left. * // u: 0, v: 500 * vertex(0, 50, 0, 0, 500); * * endShape(); * ``` * * `textureMode()` changes the coordinate system for uv coordinates. * * The parameter, `mode`, accepts two possible constants. If `NORMAL` is * passed, as in `textureMode(NORMAL)`, then the texture’s uv coordinates can * be provided in the range 0 to 1 instead of the image’s dimensions. This can * be helpful for using the same code for multiple images of different sizes. * For example, the code snippet above could be rewritten as follows: * * ```js * // Set the texture mode to use normalized coordinates. * textureMode(NORMAL); * * // Apply the image as a texture. * texture(img); * * // Draw the rectangle. * beginShape(); * * // Top-left. * // u: 0, v: 0 * vertex(0, 0, 0, 0, 0); * * // Top-right. * // u: 1, v: 0 * vertex(30, 0, 0, 1, 0); * * // Bottom-right. * // u: 1, v: 1 * vertex(30, 50, 0, 1, 1); * * // Bottom-left. * // u: 0, v: 1 * vertex(0, 50, 0, 0, 1); * * endShape(); * ``` * * By default, `mode` is `IMAGE`, which scales uv coordinates to the * dimensions of the image. Calling `textureMode(IMAGE)` applies the default. * * Note: `textureMode()` can only be used in WebGL mode. * * @method textureMode * @param {(IMAGE|NORMAL)} mode either IMAGE or NORMAL. * * @example * let img; * * async function setup() { * // Load an image and create a p5.Image object. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100, WEBGL); * * describe('An image of a ceiling against a black background.'); * } * * function draw() { * background(0); * * // Apply the image as a texture. * texture(img); * * // Draw the custom shape. * // Use the image's width and height as uv coordinates. * beginShape(); * vertex(-30, -30, 0, 0); * vertex(30, -30, img.width, 0); * vertex(30, 30, img.width, img.height); * vertex(-30, 30, 0, img.height); * endShape(); * } * * @example * let img; * * async function setup() { * // Load an image and create a p5.Image object. * img = await loadImage('assets/laDefense.jpg'); * * createCanvas(100, 100, WEBGL); * * describe('An image of a ceiling against a black background.'); * } * * function draw() { * background(0); * * // Set the texture mode. * textureMode(NORMAL); * * // Apply the image as a texture. * texture(img); * * // Draw the custom shape. * // Use normalized uv coordinates. * beginShape(); * vertex(-30, -30, 0, 0); * vertex(30, -30, 1, 0); * vertex(30, 30, 1, 1); * vertex(-30, 30, 0, 1); * endShape(); * } */ fn.textureMode = function (mode) { if (mode !== IMAGE && mode !== NORMAL) { console.warn( `You tried to set ${mode} textureMode only supports IMAGE & NORMAL `, ); } else { this._renderer.states.setValue("textureMode", mode); } }; /** * Changes the way textures behave when a shape’s uv coordinates go beyond the * texture. * * In order for texture() to work, a shape needs a * way to map the points on its surface to the pixels in an image. Built-in * shapes such as rect() and * box() already have these texture mappings based on * their vertices. Custom shapes created with * vertex() require texture mappings to be passed as * uv coordinates. * * Each call to vertex() must include 5 arguments, * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)` * to the pixel at coordinates `(u, v)` within an image. For example, the * corners of a rectangular image are mapped to the corners of a rectangle by default: * * ```js * // Apply the image as a texture. * texture(img); * * // Draw the rectangle. * rect(0, 0, 30, 50); * ``` * * If the image in the code snippet above has dimensions of 300 x 500 pixels, * the same result could be achieved as follows: * * ```js * // Apply the image as a texture. * texture(img); * * // Draw the rectangle. * beginShape(); * * // Top-left. * // u: 0, v: 0 * vertex(0, 0, 0, 0, 0); * * // Top-right. * // u: 300, v: 0 * vertex(30, 0, 0, 300, 0); * * // Bottom-right. * // u: 300, v: 500 * vertex(30, 50, 0, 300, 500); * * // Bottom-left. * // u: 0, v: 500 * vertex(0, 50, 0, 0, 500); * * endShape(); * ``` * * `textureWrap()` controls how textures behave when their uv's go beyond the * texture. Doing so can produce interesting visual effects such as tiling. * For example, the custom shape above could have u-coordinates are greater * than the image’s width: * * ```js * // Apply the image as a texture. * texture(img); * * // Draw the rectangle. * beginShape(); * vertex(0, 0, 0, 0, 0); * * // Top-right. * // u: 600 * vertex(30, 0, 0, 600, 0); * * // Bottom-right. * // u: 600 * vertex(30, 50, 0, 600, 500); * * vertex(0, 50, 0, 0, 500); * endShape(); * ``` * * The u-coordinates of 600 are greater than the texture image’s width of 300. * This creates interesting possibilities. * * The first parameter, `wrapX`, accepts three possible constants. If `CLAMP` * is passed, as in `textureWrap(CLAMP)`, the pixels at the edge of the * texture will extend to the shape’s edges. If `REPEAT` is passed, as in * `textureWrap(REPEAT)`, the texture will tile repeatedly until reaching the * shape’s edges. If `MIRROR` is passed, as in `textureWrap(MIRROR)`, the * texture will tile repeatedly until reaching the shape’s edges, flipping * its orientation between tiles. By default, textures `CLAMP`. * * The second parameter, `wrapY`, is optional. It accepts the same three * constants, `CLAMP`, `REPEAT`, and `MIRROR`. If one of these constants is * passed, as in `textureWRAP(MIRROR, REPEAT)`, then the texture will `MIRROR` * horizontally and `REPEAT` vertically. By default, `wrapY` will be set to * the same value as `wrapX`. * * Note: `textureWrap()` can only be used in WebGL mode. * * @method textureWrap * @param {(CLAMP|REPEAT|MIRROR)} wrapX either CLAMP, REPEAT, or MIRROR * @param {(CLAMP|REPEAT|MIRROR)} [wrapY=wrapX] either CLAMP, REPEAT, or MIRROR * * @example * let img; * * async function setup() { * img = await loadImage('assets/rockies128.jpg'); * * createCanvas(100, 100, WEBGL); * * describe( * 'An image of a landscape occupies the top-left corner of a square. Its edge colors smear to cover the other thre quarters of the square.' * ); * } * * function draw() { * background(0); * * // Set the texture mode. * textureMode(NORMAL); * * // Set the texture wrapping. * // Note: CLAMP is the default mode. * textureWrap(CLAMP); * * // Apply the image as a texture. * texture(img); * * // Style the shape. * noStroke(); * * // Draw the shape. * // Use uv coordinates > 1. * beginShape(); * vertex(-30, -30, 0, 0, 0); * vertex(30, -30, 0, 2, 0); * vertex(30, 30, 0, 2, 2); * vertex(-30, 30, 0, 0, 2); * endShape(); * } * * @example * let img; * * async function setup() { * img = await loadImage('assets/rockies128.jpg'); * * createCanvas(100, 100, WEBGL); * * describe('Four identical images of a landscape arranged in a grid.'); * } * * function draw() { * background(0); * * // Set the texture mode. * textureMode(NORMAL); * * // Set the texture wrapping. * textureWrap(REPEAT); * * // Apply the image as a texture. * texture(img); * * // Style the shape. * noStroke(); * * // Draw the shape. * // Use uv coordinates > 1. * beginShape(); * vertex(-30, -30, 0, 0, 0); * vertex(30, -30, 0, 2, 0); * vertex(30, 30, 0, 2, 2); * vertex(-30, 30, 0, 0, 2); * endShape(); * } * * @example * let img; * * async function setup() { * img = await loadImage('assets/rockies128.jpg'); * * createCanvas(100, 100, WEBGL); * * describe( * 'Four identical images of a landscape arranged in a grid. The images are reflected horizontally and vertically, creating a kaleidoscope effect.' * ); * } * * function draw() { * background(0); * * // Set the texture mode. * textureMode(NORMAL); * * // Set the texture wrapping. * textureWrap(MIRROR); * * // Apply the image as a texture. * texture(img); * * // Style the shape. * noStroke(); * * // Draw the shape. * // Use uv coordinates > 1. * beginShape(); * vertex(-30, -30, 0, 0, 0); * vertex(30, -30, 0, 2, 0); * vertex(30, 30, 0, 2, 2); * vertex(-30, 30, 0, 0, 2); * endShape(); * } * * @example * let img; * * async function setup() { * img = await loadImage('assets/rockies128.jpg'); * * createCanvas(100, 100, WEBGL); * * describe( * 'Four identical images of a landscape arranged in a grid. The top row and bottom row are reflections of each other.' * ); * } * * function draw() { * background(0); * * // Set the texture mode. * textureMode(NORMAL); * * // Set the texture wrapping. * textureWrap(REPEAT, MIRROR); * * // Apply the image as a texture. * texture(img); * * // Style the shape. * noStroke(); * * // Draw the shape. * // Use uv coordinates > 1. * beginShape(); * vertex(-30, -30, 0, 0, 0); * vertex(30, -30, 0, 2, 0); * vertex(30, 30, 0, 2, 2); * vertex(-30, 30, 0, 0, 2); * endShape(); * } */ fn.textureWrap = function (wrapX, wrapY = wrapX) { this._renderer.states.setValue("textureWrapX", wrapX); this._renderer.states.setValue("textureWrapY", wrapY); for (const texture of this._renderer.textures.values()) { texture.setWrapMode(wrapX, wrapY); } }; /** * Sets the current material as a normal material. * * A normal material sets surfaces facing the x-axis to red, those facing the * y-axis to green, and those facing the z-axis to blue. Normal material isn't * affected by light. It’s often used as a placeholder material when debugging. * * Note: `normalMaterial()` can only be used in WebGL mode. * * @method normalMaterial * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A multicolor torus drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Style the torus. * normalMaterial(); * * // Draw the torus. * torus(30); * } */ fn.normalMaterial = function (...args) { this._assert3d("normalMaterial"); // p5._validateParameters('normalMaterial', args); this._renderer.normalMaterial(...args); return this; }; /** * Sets the ambient color of shapes’ surface material. * * The `ambientMaterial()` color sets the components of the * ambientLight() color that shapes will * reflect. For example, calling `ambientMaterial(255, 255, 0)` would cause a * shape to reflect red and green light, but not blue light. * * `ambientMaterial()` can be called three ways with different parameters to * set the material’s color. * * The first way to call `ambientMaterial()` has one parameter, `gray`. * Grayscale values between 0 and 255, as in `ambientMaterial(50)`, can be * passed to set the material’s color. Higher grayscale values make shapes * appear brighter. * * The second way to call `ambientMaterial()` has one parameter, `color`. A * p5.Color object, an array of color values, or a * CSS color string, as in `ambientMaterial('magenta')`, can be passed to set * the material’s color. * * The third way to call `ambientMaterial()` has three parameters, `v1`, `v2`, * and `v3`. RGB, HSB, or HSL values, as in `ambientMaterial(255, 0, 0)`, can * be passed to set the material’s colors. Color values will be interpreted * using the current colorMode(). * * Note: `ambientMaterial()` can only be used in WebGL mode. * * @method ambientMaterial * @param {Number} v1 red or hue value in the current * colorMode(). * @param {Number} v2 green or saturation value in the * current colorMode(). * @param {Number} v3 blue, brightness, or lightness value in the * current colorMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A magenta cube drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a magenta ambient light. * ambientLight(255, 0, 255); * * // Draw the box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A purple cube drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a magenta ambient light. * ambientLight(255, 0, 255); * * // Add a dark gray ambient material. * ambientMaterial(150); * * // Draw the box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A red cube drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a magenta ambient light. * ambientLight(255, 0, 255); * * // Add a yellow ambient material using RGB values. * ambientMaterial(255, 255, 0); * * // Draw the box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A red cube drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a magenta ambient light. * ambientLight(255, 0, 255); * * // Add a yellow ambient material using a p5.Color object. * let c = color(255, 255, 0); * ambientMaterial(c); * * // Draw the box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A red cube drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a magenta ambient light. * ambientLight(255, 0, 255); * * // Add a yellow ambient material using a color string. * ambientMaterial('yellow'); * * // Draw the box. * box(); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A yellow cube drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a white ambient light. * ambientLight(255, 255, 255); * * // Add a yellow ambient material using a color string. * ambientMaterial('yellow'); * * // Draw the box. * box(); * } */ /** * @method ambientMaterial * @param {Number} gray grayscale value between 0 (black) and 255 (white). * @chainable */ /** * @method ambientMaterial * @param {p5.Color|Number[]|String} color * color as a p5.Color object, * an array of color values, or a CSS string. * @chainable */ fn.ambientMaterial = function (v1, v2, v3) { this._assert3d("ambientMaterial"); // p5._validateParameters('ambientMaterial', arguments); const color = fn.color.apply(this, arguments); this._renderer.states.setValue("_hasSetAmbient", true); this._renderer.states.setValue("curAmbientColor", color._array); this._renderer.states.setValue("_useNormalMaterial", false); this._renderer.states.setValue("enableLighting", true); if (!this._renderer.states.fillColor) { this._renderer.states.setValue("fillColor", new Color([1, 1, 1])); } return this; }; /** * Sets the emissive color of shapes’ surface material. * * The `emissiveMaterial()` color sets a color shapes display at full * strength, regardless of lighting. This can give the appearance that a shape * is glowing. However, emissive materials don’t actually emit light that * can affect surrounding objects. * * `emissiveMaterial()` can be called three ways with different parameters to * set the material’s color. * * The first way to call `emissiveMaterial()` has one parameter, `gray`. * Grayscale values between 0 and 255, as in `emissiveMaterial(50)`, can be * passed to set the material’s color. Higher grayscale values make shapes * appear brighter. * * The second way to call `emissiveMaterial()` has one parameter, `color`. A * p5.Color object, an array of color values, or a * CSS color string, as in `emissiveMaterial('magenta')`, can be passed to set * the material’s color. * * The third way to call `emissiveMaterial()` has four parameters, `v1`, `v2`, * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be * passed to set the material’s colors, as in `emissiveMaterial(255, 0, 0)` or * `emissiveMaterial(255, 0, 0, 30)`. Color values will be interpreted using * the current colorMode(). * * Note: `emissiveMaterial()` can only be used in WebGL mode. * * @method emissiveMaterial * @param {Number} v1 red or hue value in the current * colorMode(). * @param {Number} v2 green or saturation value in the * current colorMode(). * @param {Number} v3 blue, brightness, or lightness value in the * current colorMode(). * @param {Number} [alpha] alpha value in the current * colorMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A red cube drawn on a gray background.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a white ambient light. * ambientLight(255, 255, 255); * * // Add a red emissive material using RGB values. * emissiveMaterial(255, 0, 0); * * // Draw the box. * box(); * } */ /** * @method emissiveMaterial * @param {Number} gray grayscale value between 0 (black) and 255 (white). * @chainable */ /** * @method emissiveMaterial * @param {p5.Color|Number[]|String} color * color as a p5.Color object, * an array of color values, or a CSS string. * @chainable */ fn.emissiveMaterial = function (v1, v2, v3, a) { this._assert3d("emissiveMaterial"); // p5._validateParameters('emissiveMaterial', arguments); const color = fn.color.apply(this, arguments); this._renderer.states.setValue("curEmissiveColor", color._array); this._renderer.states.setValue("_useEmissiveMaterial", true); this._renderer.states.setValue("_useNormalMaterial", false); this._renderer.states.setValue("enableLighting", true); return this; }; /** * Sets the specular color of shapes’ surface material. * * The `specularMaterial()` color sets the components of light color that * glossy coats on shapes will reflect. For example, calling * `specularMaterial(255, 255, 0)` would cause a shape to reflect red and * green light, but not blue light. * * Unlike ambientMaterial(), * `specularMaterial()` will reflect the full color of light sources including * directionalLight(), * pointLight(), * and spotLight(). This is what gives it shapes * their "shiny" appearance. The material’s shininess can be controlled by the * shininess() function. * * `specularMaterial()` can be called three ways with different parameters to * set the material’s color. * * The first way to call `specularMaterial()` has one parameter, `gray`. * Grayscale values between 0 and 255, as in `specularMaterial(50)`, can be * passed to set the material’s color. Higher grayscale values make shapes * appear brighter. * * The second way to call `specularMaterial()` has one parameter, `color`. A * p5.Color> object, an array of color values, or a CSS * color string, as in `specularMaterial('magenta')`, can be passed to set the * material’s color. * * The third way to call `specularMaterial()` has four parameters, `v1`, `v2`, * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be * passed to set the material’s colors, as in `specularMaterial(255, 0, 0)` or * `specularMaterial(255, 0, 0, 30)`. Color values will be interpreted using * the current colorMode(). * * @method specularMaterial * @param {Number} gray grayscale value between 0 (black) and 255 (white). * @param {Number} [alpha] alpha value in the current current * colorMode(). * @chainable * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click the canvas to apply a specular material. * * let isGlossy = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe('A red torus drawn on a gray background. It becomes glossy when the user double-clicks.'); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a white point light at the top-right. * pointLight(255, 255, 255, 30, -40, 30); * * // Add a glossy coat if the user has double-clicked. * if (isGlossy === true) { * specularMaterial(255); * shininess(50); * } * * // Style the torus. * noStroke(); * fill(255, 0, 0); * * // Draw the torus. * torus(30); * } * * // Make the torus glossy when the user double-clicks. * function doubleClicked() { * isGlossy = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click the canvas to apply a specular material. * * let isGlossy = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a white point light at the top-right. * pointLight(255, 255, 255, 30, -40, 30); * * // Add a glossy green coat if the user has double-clicked. * if (isGlossy === true) { * specularMaterial(0, 255, 0); * shininess(50); * } * * // Style the torus. * noStroke(); * fill(255, 0, 0); * * // Draw the torus. * torus(30); * } * * // Make the torus glossy when the user double-clicks. * function doubleClicked() { * isGlossy = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click the canvas to apply a specular material. * * let isGlossy = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a white point light at the top-right. * pointLight(255, 255, 255, 30, -40, 30); * * // Add a glossy green coat if the user has double-clicked. * if (isGlossy === true) { * // Create a p5.Color object. * let c = color('green'); * specularMaterial(c); * shininess(50); * } * * // Style the torus. * noStroke(); * fill(255, 0, 0); * * // Draw the torus. * torus(30); * } * * // Make the torus glossy when the user double-clicks. * function doubleClicked() { * isGlossy = true; * } * * @example * // Click and drag the mouse to view the scene from different angles. * // Double-click the canvas to apply a specular material. * * let isGlossy = false; * * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * * // Turn on a white point light at the top-right. * pointLight(255, 255, 255, 30, -40, 30); * * // Add a glossy green coat if the user has double-clicked. * if (isGlossy === true) { * specularMaterial('#00FF00'); * shininess(50); * } * * // Style the torus. * noStroke(); * fill(255, 0, 0); * * // Draw the torus. * torus(30); * } * * // Make the torus glossy when the user double-clicks. * function doubleClicked() { * isGlossy = true; * } */ /** * @method specularMaterial * @param {Number} v1 red or hue value in * the current colorMode(). * @param {Number} v2 green or saturation value * in the current colorMode(). * @param {Number} v3 blue, brightness, or lightness value * in the current colorMode(). * @param {Number} [alpha] * @chainable */ /** * @method specularMaterial * @param {p5.Color|Number[]|String} color * color as a p5.Color object, * an array of color values, or a CSS string. * @chainable */ fn.specularMaterial = function (v1, v2, v3, alpha) { this._assert3d("specularMaterial"); // p5._validateParameters('specularMaterial', arguments); const color = fn.color.apply(this, arguments); this._renderer.states.setValue("curSpecularColor", color._array); this._renderer.states.setValue("_useSpecularMaterial", true); this._renderer.states.setValue("_useNormalMaterial", false); this._renderer.states.setValue("enableLighting", true); return this; }; /** * Sets the amount of gloss ("shininess") of a * specularMaterial(). * * Shiny materials focus reflected light more than dull materials. * `shininess()` affects the way materials reflect light sources including * directionalLight(), * pointLight(), * and spotLight(). * * The parameter, `shine`, is a number that sets the amount of shininess. * `shine` must be greater than 1, which is its default value. * * @method shininess * @param {Number} shine amount of shine. * @chainable * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'Two red spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is shinier than the left sphere.' * ); * } * * function draw() { * background(200); * * // Turn on a red ambient light. * ambientLight(255, 0, 0); * * // Get the mouse's coordinates. * let mx = mouseX - 50; * let my = mouseY - 50; * * // Turn on a white point light that follows the mouse. * pointLight(255, 255, 255, mx, my, 50); * * // Style the sphere. * noStroke(); * * // Add a specular material with a grayscale value. * specularMaterial(255); * * // Draw the left sphere with low shininess. * translate(-25, 0, 0); * shininess(10); * sphere(20); * * // Draw the right sphere with high shininess. * translate(50, 0, 0); * shininess(100); * sphere(20); * } */ fn.shininess = function (shine) { this._assert3d("shininess"); // p5._validateParameters('shininess', arguments); this._renderer.shininess(shine); return this; }; /** * Sets the amount of "metalness" of a * specularMaterial(). * * `metalness()` can make materials appear more metallic. It affects the way * materials reflect light sources including * affects the way materials reflect light sources including * directionalLight(), * pointLight(), * spotLight(), and * imageLight(). * * The parameter, `metallic`, is a number that sets the amount of metalness. * `metallic` must be greater than 1, which is its default value. Higher * values, such as `metalness(100)`, make specular materials appear more * metallic. * * @method metalness * @param {Number} metallic amount of metalness. * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * describe( * 'Two blue spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is more metallic than the left sphere.' * ); * } * * function draw() { * background(200); * * // Turn on an ambient light. * ambientLight(200); * * // Get the mouse's coordinates. * let mx = mouseX - 50; * let my = mouseY - 50; * * // Turn on a white point light that follows the mouse. * pointLight(255, 255, 255, mx, my, 50); * * // Style the spheres. * noStroke(); * fill(30, 30, 255); * specularMaterial(255); * shininess(20); * * // Draw the left sphere with low metalness. * translate(-25, 0, 0); * metalness(1); * sphere(20); * * // Draw the right sphere with high metalness. * translate(50, 0, 0); * metalness(50); * sphere(20); * } * * @example * // Click and drag the mouse to view the scene from different angles. * * let img; * * async function setup() { * img = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(100 ,100 ,WEBGL); * * describe( * 'Two spheres floating above a landscape. The surface of the spheres reflect the landscape. The right sphere is more reflective than the left sphere.' * ); * } * * function draw() { * // Add the panorama. * panorama(img); * * // Enable orbiting with the mouse. * orbitControl(); * * // Use the image as a light source. * imageLight(img); * * // Style the spheres. * noStroke(); * specularMaterial(50); * shininess(200); * * // Draw the left sphere with low metalness. * translate(-25, 0, 0); * metalness(1); * sphere(20); * * // Draw the right sphere with high metalness. * translate(50, 0, 0); * metalness(50); * sphere(20); * } */ fn.metalness = function (metallic) { this._assert3d("metalness"); this._renderer.metalness(metallic); return this; }; Renderer3D.prototype.shader = function (s) { // Always set the shader as a fill shader this.states.setValue("userFillShader", s); this.states.setValue("_useNormalMaterial", false); s.ensureCompiledOnContext(this); s.setDefaultUniforms(); }; Renderer3D.prototype.strokeShader = function (s) { this.states.setValue("userStrokeShader", s); s.ensureCompiledOnContext(this); s.setDefaultUniforms(); }; Renderer3D.prototype.imageShader = function (s) { this.states.setValue("userImageShader", s); s.ensureCompiledOnContext(this); s.setDefaultUniforms(); }; Renderer3D.prototype.resetShader = function () { this.states.setValue("userFillShader", null); this.states.setValue("userStrokeShader", null); this.states.setValue("userImageShader", null); }; Renderer3D.prototype.texture = function (tex) { this.states.setValue("drawMode", TEXTURE); this.states.setValue("_useNormalMaterial", false); this.states.setValue("_tex", tex); this.states.setValue("fillColor", new Color([1, 1, 1])); }; Renderer3D.prototype.normalMaterial = function (...args) { this.states.setValue("drawMode", FILL); this.states.setValue("_useSpecularMaterial", false); this.states.setValue("_useEmissiveMaterial", false); this.states.setValue("_useNormalMaterial", true); this.states.setValue("curFillColor", [1, 1, 1, 1]); this.states.setValue("fillColor", new Color([1, 1, 1])); this.states.setValue("strokeColor", null); }; // Renderer3D.prototype.ambientMaterial = function(v1, v2, v3) { // } // Renderer3D.prototype.emissiveMaterial = function(v1, v2, v3, a) { // } // Renderer3D.prototype.specularMaterial = function(v1, v2, v3, alpha) { // } Renderer3D.prototype.shininess = function (shine) { if (shine < 1) { shine = 1; } this.states.setValue("_useShininess", shine); }; Renderer3D.prototype.metalness = function (metallic) { const metalMix = 1 - Math.exp(-metallic / 100); this.states.setValue("_useMetalness", metalMix); }; } if (typeof p5 !== "undefined") { loading(p5, p5.prototype); } /** * @module Math * @submodule Trigonometry * @for p5 * @requires core * @requires constants */ function trigonometry(p5, fn){ /** * A `String` constant that's used to set the * angleMode(). * * By default, functions such as rotate() and * sin() expect angles measured in units of radians. * Calling `angleMode(DEGREES)` ensures that angles are measured in units of * degrees. * * Note: `TWO_PI` radians equals 360˚. * * @typedef {'degrees'} DEGREES * @property {DEGREES} DEGREES * @final * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw a red arc from 0 to HALF_PI radians. * fill(255, 0, 0); * arc(50, 50, 80, 80, 0, HALF_PI); * * // Use degrees. * angleMode(DEGREES); * * // Draw a blue arc from 90˚ to 180˚. * fill(0, 0, 255); * arc(50, 50, 80, 80, 90, 180); * * describe('The bottom half of a circle drawn on a gray background. The bottom-right quarter is red. The bottom-left quarter is blue.'); * } */ const DEGREES = fn.DEGREES = 'degrees'; /** * A `String` constant that's used to set the * angleMode(). * * By default, functions such as rotate() and * sin() expect angles measured in units of radians. * Calling `angleMode(RADIANS)` ensures that angles are measured in units of * radians. Doing so can be useful if the * angleMode() has been set to * DEGREES. * * Note: `TWO_PI` radians equals 360˚. * * @typedef {'radians'} RADIANS * @property {RADIANS} RADIANS * @final * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use degrees. * angleMode(DEGREES); * * // Draw a red arc from 0˚ to 90˚. * fill(255, 0, 0); * arc(50, 50, 80, 80, 0, 90); * * // Use radians. * angleMode(RADIANS); * * // Draw a blue arc from HALF_PI to PI. * fill(0, 0, 255); * arc(50, 50, 80, 80, HALF_PI, PI); * * describe('The bottom half of a circle drawn on a gray background. The bottom-right quarter is red. The bottom-left quarter is blue.'); * } */ const RADIANS = fn.RADIANS = 'radians'; /* * all DEGREES/RADIANS conversion should be done in the p5 instance * if possible, using the p5._toRadians(), p5._fromRadians() methods. */ fn._angleMode = RADIANS; /** * Calculates the arc cosine of a number. * * `acos()` is the inverse of cos(). It expects * arguments in the range -1 to 1. By default, `acos()` returns values in the * range 0 to π (about 3.14). If the * angleMode() is `DEGREES`, then values are * returned in the range 0 to 180. * * @method acos * @param {Number} value value whose arc cosine is to be returned. * @return {Number} arc cosine of the given value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Calculate cos() and acos() values. * let a = PI; * let c = cos(a); * let ac = acos(c); * * // Display the values. * text(`${round(a, 3)}`, 35, 25); * text(`${round(c, 3)}`, 35, 50); * text(`${round(ac, 3)}`, 35, 75); * * describe('The numbers 3.142, -1, and 3.142 written on separate rows.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Calculate cos() and acos() values. * let a = PI + QUARTER_PI; * let c = cos(a); * let ac = acos(c); * * // Display the values. * text(`${round(a, 3)}`, 35, 25); * text(`${round(c, 3)}`, 35, 50); * text(`${round(ac, 3)}`, 35, 75); * * describe('The numbers 3.927, -0.707, and 2.356 written on separate rows.'); * } */ fn.acos = function(ratio) { return this._fromRadians(Math.acos(ratio)); }; /** * Calculates the arc sine of a number. * * `asin()` is the inverse of sin(). It expects input * values in the range of -1 to 1. By default, `asin()` returns values in the * range -π ÷ 2 (about -1.57) to π ÷ 2 (about 1.57). If * the angleMode() is `DEGREES` then values are * returned in the range -90 to 90. * * @method asin * @param {Number} value value whose arc sine is to be returned. * @return {Number} arc sine of the given value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Calculate sin() and asin() values. * let a = PI / 3; * let s = sin(a); * let as = asin(s); * * // Display the values. * text(`${round(a, 3)}`, 35, 25); * text(`${round(s, 3)}`, 35, 50); * text(`${round(as, 3)}`, 35, 75); * * describe('The numbers 1.047, 0.866, and 1.047 written on separate rows.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Calculate sin() and asin() values. * let a = PI + PI / 3; * let s = sin(a); * let as = asin(s); * * // Display the values. * text(`${round(a, 3)}`, 35, 25); * text(`${round(s, 3)}`, 35, 50); * text(`${round(as, 3)}`, 35, 75); * * describe('The numbers 4.189, -0.866, and -1.047 written on separate rows.'); * } */ fn.asin = function(ratio) { return this._fromRadians(Math.asin(ratio)); }; /** * Calculates the arc tangent of a number. * * `atan()` is the inverse of tan(). It expects input * values in the range of -Infinity to Infinity. By default, `atan()` returns * values in the range -π ÷ 2 (about -1.57) to π ÷ 2 * (about 1.57). If the angleMode() is `DEGREES` * then values are returned in the range -90 to 90. * * @method atan * @param {Number} value value whose arc tangent is to be returned. * @return {Number} arc tangent of the given value. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Calculate tan() and atan() values. * let a = PI / 3; * let t = tan(a); * let at = atan(t); * * // Display the values. * text(`${round(a, 3)}`, 35, 25); * text(`${round(t, 3)}`, 35, 50); * text(`${round(at, 3)}`, 35, 75); * * describe('The numbers 1.047, 1.732, and 1.047 written on separate rows.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Calculate tan() and atan() values. * let a = PI + PI / 3; * let t = tan(a); * let at = atan(t); * * // Display the values. * text(`${round(a, 3)}`, 35, 25); * text(`${round(t, 3)}`, 35, 50); * text(`${round(at, 3)}`, 35, 75); * * describe('The numbers 4.189, 1.732, and 1.047 written on separate rows.'); * } */ fn.atan = function(ratio) { return this._fromRadians(Math.atan(ratio)); }; /** * Calculates the angle formed by a point, the origin, and the positive * x-axis. * * `atan2()` is most often used for orienting geometry to the mouse's * position, as in `atan2(mouseY, mouseX)`. The first parameter is the point's * y-coordinate and the second parameter is its x-coordinate. * * By default, `atan2()` returns values in the range * -π (about -3.14) to π (3.14). If the * angleMode() is `DEGREES`, then values are * returned in the range -180 to 180. * * @method atan2 * @param {Number} y y-coordinate of the point. * @param {Number} x x-coordinate of the point. * @return {Number} arc tangent of the given point. * * @example * function setup() { * createCanvas(100, 100); * * describe('A rectangle at the top-left of the canvas rotates with mouse movements.'); * } * * function draw() { * background(200); * * // Calculate the angle between the mouse * // and the origin. * let a = atan2(mouseY, mouseX); * * // Rotate. * rotate(a); * * // Draw the shape. * rect(0, 0, 60, 10); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A rectangle at the center of the canvas rotates with mouse movements.'); * } * * function draw() { * background(200); * * // Translate the origin to the center. * translate(50, 50); * * // Get the mouse's coordinates relative to the origin. * let x = mouseX - 50; * let y = mouseY - 50; * * // Calculate the angle between the mouse and the origin. * let a = atan2(y, x); * * // Rotate. * rotate(a); * * // Draw the shape. * rect(-30, -5, 60, 10); * } */ fn.atan2 = function(y, x) { return this._fromRadians(Math.atan2(y, x)); }; /** * Calculates the cosine of an angle. * * `cos()` is useful for many geometric tasks in creative coding. The values * returned oscillate between -1 and 1 as the input angle increases. `cos()` * calculates the cosine of an angle, using radians by default, or according * to if angleMode() setting (RADIANS or DEGREES). * * @method cos * @param {Number} angle the angle, in radians by default, or according to if angleMode() setting (RADIANS or DEGREES). * @return {Number} cosine of the angle. * * @example * function setup() { * createCanvas(100, 100); * * describe('A white ball on a string oscillates left and right.'); * } * * function draw() { * background(200); * * // Calculate the coordinates. * let x = 30 * cos(frameCount * 0.05) + 50; * let y = 50; * * // Draw the oscillator. * line(50, y, x, y); * circle(x, y, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * describe('A series of black dots form a wave pattern.'); * } * * function draw() { * // Calculate the coordinates. * let x = frameCount; * let y = 30 * cos(x * 0.1) + 50; * * // Draw the point. * point(x, y); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * describe('A series of black dots form an infinity symbol.'); * } * * function draw() { * // Calculate the coordinates. * let x = 30 * cos(frameCount * 0.1) + 50; * let y = 10 * sin(frameCount * 0.2) + 50; * * // Draw the point. * point(x, y); * } */ fn.cos = function(angle) { return Math.cos(this._toRadians(angle)); }; /** * Calculates the sine of an angle. * * `sin()` is useful for many geometric tasks in creative coding. The values * returned oscillate between -1 and 1 as the input angle increases. `sin()` * calculates the sine of an angle, using radians by default, or according to * if angleMode() setting (RADIANS or DEGREES). * * @method sin * @param {Number} angle the angle, in radians by default, or according to if angleMode() setting (RADIANS or DEGREES). * @return {Number} sine of the angle. * * @example * function setup() { * createCanvas(100, 100); * * describe('A white ball on a string oscillates up and down.'); * } * * function draw() { * background(200); * * // Calculate the coordinates. * let x = 50; * let y = 30 * sin(frameCount * 0.05) + 50; * * // Draw the oscillator. * line(50, y, x, y); * circle(x, y, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * describe('A series of black dots form a wave pattern.'); * } * * function draw() { * // Calculate the coordinates. * let x = frameCount; * let y = 30 * sin(x * 0.1) + 50; * * // Draw the point. * point(x, y); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * describe('A series of black dots form an infinity symbol.'); * } * * function draw() { * // Calculate the coordinates. * let x = 30 * cos(frameCount * 0.1) + 50; * let y = 10 * sin(frameCount * 0.2) + 50; * * // Draw the point. * point(x, y); * } */ fn.sin = function(angle) { return Math.sin(this._toRadians(angle)); }; /** * Calculates the tangent of an angle. * * `tan()` is useful for many geometric tasks in creative coding. The values * returned range from -Infinity to Infinity and repeat periodically as the * input angle increases. `tan()` calculates the tan of an angle, using radians * by default, or according to * if angleMode() setting (RADIANS or DEGREES). * * @method tan * @param {Number} angle the angle, in radians by default, or according to if angleMode() setting (RADIANS or DEGREES). * @return {Number} tangent of the angle. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * describe('A series of identical curves drawn with black dots. Each curve starts from the top of the canvas, continues down at a slight angle, flattens out at the middle of the canvas, then continues to the bottom.'); * } * * function draw() { * // Calculate the coordinates. * let x = frameCount; * let y = 5 * tan(x * 0.1) + 50; * * // Draw the point. * point(x, y); * } */ fn.tan = function(angle) { return Math.tan(this._toRadians(angle)); }; /** * Converts an angle measured in radians to its value in degrees. * * Degrees and radians are both units for measuring angles. There are 360˚ in * one full rotation. A full rotation is 2 × π (about 6.28) radians. * * The same angle can be expressed in with either unit. For example, 90° is a * quarter of a full rotation. The same angle is 2 × π ÷ 4 * (about 1.57) radians. * * @method degrees * @param {Number} radians radians value to convert to degrees. * @return {Number} converted angle. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Calculate the angle conversion. * let rad = QUARTER_PI; * let deg = degrees(rad); * * // Display the conversion. * text(`${round(rad, 2)} rad = ${deg}˚`, 10, 50); * * describe('The text "0.79 rad = 45˚".'); * } */ fn.degrees = angle => angle * RAD_TO_DEG; /** * Converts an angle measured in degrees to its value in radians. * * Degrees and radians are both units for measuring angles. There are 360˚ in * one full rotation. A full rotation is 2 × π (about 6.28) radians. * * The same angle can be expressed in with either unit. For example, 90° is a * quarter of a full rotation. The same angle is 2 × π ÷ 4 * (about 1.57) radians. * * @method radians * @param {Number} degrees degree value to convert to radians. * @return {Number} converted angle. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Caclulate the angle conversion. * let deg = 45; * let rad = radians(deg); * * // Display the angle conversion. * text(`${deg}˚ = ${round(rad, 3)} rad`, 10, 50); * * describe('The text "45˚ = 0.785 rad".'); * } */ fn.radians = angle => angle * DEG_TO_RAD; /** * Changes the unit system used to measure angles. * * Degrees and radians are both units for measuring angles. There are 360˚ in * one full rotation. A full rotation is 2 × π (about 6.28) radians. * * Functions such as rotate() and * sin() expect angles measured radians by default. * Calling `angleMode(DEGREES)` switches to degrees. Calling * `angleMode(RADIANS)` switches back to radians. * * Calling `angleMode()` with no arguments returns current angle mode, which * is either `RADIANS` or `DEGREES`. * * @method angleMode * @param {(RADIANS|DEGREES)} mode either RADIANS or DEGREES. * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Rotate 1/8 turn. * rotate(QUARTER_PI); * * // Draw a line. * line(0, 0, 80, 0); * * describe('A diagonal line radiating from the top-left corner of a square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Use degrees. * angleMode(DEGREES); * * // Rotate 1/8 turn. * rotate(45); * * // Draw a line. * line(0, 0, 80, 0); * * describe('A diagonal line radiating from the top-left corner of a square.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Calculate the angle to rotate. * let angle = TWO_PI / 7; * * // Move the origin to the center. * translate(50, 50); * * // Style the flower. * noStroke(); * fill(255, 50); * * // Draw the flower. * for (let i = 0; i < 7; i += 1) { * ellipse(0, 0, 80, 20); * rotate(angle); * } * * describe('A translucent white flower on a dark background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * background(50); * * // Use degrees. * angleMode(DEGREES); * * // Calculate the angle to rotate. * let angle = 360 / 7; * * // Move the origin to the center. * translate(50, 50); * * // Style the flower. * noStroke(); * fill(255, 50); * * // Draw the flower. * for (let i = 0; i < 7; i += 1) { * ellipse(0, 0, 80, 20); * rotate(angle); * } * * describe('A translucent white flower on a dark background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * describe('A white ball on a string oscillates left and right.'); * } * * function draw() { * background(200); * * // Calculate the coordinates. * let x = 30 * cos(frameCount * 0.05) + 50; * let y = 50; * * // Draw the oscillator. * line(50, y, x, y); * circle(x, y, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * // Use degrees. * angleMode(DEGREES); * * describe('A white ball on a string oscillates left and right.'); * } * * function draw() { * background(200); * * // Calculate the coordinates. * let x = 30 * cos(frameCount * 2.86) + 50; * let y = 50; * * // Draw the oscillator. * line(50, y, x, y); * circle(x, y, 20); * } * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the upper line. * rotate(PI / 6); * line(0, 0, 80, 0); * * // Use degrees. * angleMode(DEGREES); * * // Draw the lower line. * rotate(30); * line(0, 0, 80, 0); * * describe('Two diagonal lines radiating from the top-left corner of a square. The lines are oriented 30 degrees from the edges of the square and 30 degrees apart from each other.'); * } */ /** * @method angleMode * @return {(RADIANS|DEGREES)} mode either RADIANS or DEGREES */ fn.angleMode = function(mode) { // p5._validateParameters('angleMode', arguments); if (typeof mode === 'undefined') { return this._angleMode; } else if (mode === DEGREES || mode === RADIANS) { const prevMode = this._angleMode; // No change if(mode === prevMode) return; // Otherwise adjust pRotation according to new mode // This is necessary for acceleration events to work properly if(mode === RADIANS) { // Change pRotation to radians this.pRotationX = this.pRotationX * DEG_TO_RAD; this.pRotationY = this.pRotationY * DEG_TO_RAD; this.pRotationZ = this.pRotationZ * DEG_TO_RAD; } else { // Change pRotation to degrees this.pRotationX = this.pRotationX * RAD_TO_DEG; this.pRotationY = this.pRotationY * RAD_TO_DEG; this.pRotationZ = this.pRotationZ * RAD_TO_DEG; } this._angleMode = mode; } }; /** * converts angles from the current angleMode to RADIANS * * @method _toRadians * @private * @param {Number} angle * @returns {Number} */ fn._toRadians = function(angle) { if (this._angleMode === DEGREES) { return angle * DEG_TO_RAD; } return angle; }; /** * converts angles from the current angleMode to DEGREES * * @method _toDegrees * @private * @param {Number} angle * @returns {Number} */ fn._toDegrees = function(angle) { if (this._angleMode === RADIANS) { return angle * RAD_TO_DEG; } return angle; }; /** * converts angles from RADIANS into the current angleMode * * @method _fromRadians * @private * @param {Number} angle * @returns {Number} */ fn._fromRadians = function(angle) { if (this._angleMode === DEGREES) { return angle * RAD_TO_DEG; } return angle; }; /** * converts angles from DEGREES into the current angleMode * * @method _fromDegrees * @private * @param {Number} angle * @returns {Number} */ fn._fromDegrees = function(angle) { if (this._angleMode === RADIANS) { return angle * DEG_TO_RAD; } return angle; }; } if(typeof p5 !== 'undefined'){ trigonometry(p5, p5.prototype); } /** * @module Rendering * @submodule Rendering * @for p5 */ class Graphics { constructor(w, h, renderer, pInst, canvas) { const r = renderer || P2D; this._pInst = pInst; this._renderer = new renderers[r](this, w, h, false, canvas); this._initializeInstanceVariables(this); this._renderer._applyDefaults(); return this; } // This is to correctly extend the p5.Element interface get elt() { return this.canvas; } get deltaTime(){ return this._pInst.deltaTime; } get canvas(){ return this._renderer?.canvas; } get drawingContext(){ return this._renderer.drawingContext; } get width(){ return this._renderer?.width; } get height(){ return this._renderer?.height; } get pixels(){ return this._renderer?.pixels; } pixelDensity(val){ let returnValue; if (typeof val === 'number') { if (val !== this._renderer._pixelDensity) { this._renderer._pixelDensity = val; } returnValue = this; this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas } else { returnValue = this._renderer._pixelDensity; } return returnValue; } resizeCanvas(w, h){ this._renderer.resize(w, h); } /** * Resets the graphics buffer's transformations and lighting. * * By default, the main canvas resets certain transformation and lighting * values each time draw() executes. `p5.Graphics` * objects must reset these values manually by calling `myGraphics.reset()`. * * @example * let pg; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object. * pg = createGraphics(60, 60); * * describe('A white circle moves downward slowly within a dark square. The circle resets at the top of the dark square when the user presses the mouse.'); * } * * function draw() { * background(200); * * // Translate the p5.Graphics object's coordinate system. * // The translation accumulates; the white circle moves. * pg.translate(0, 0.1); * * // Draw to the p5.Graphics object. * pg.background(100); * pg.circle(30, 0, 10); * * // Display the p5.Graphics object. * image(pg, 20, 20); * * // Translate the main canvas' coordinate system. * // The translation doesn't accumulate; the dark * // square is always in the same place. * translate(0, 0.1); * * // Reset the p5.Graphics object when the * // user presses the mouse. * if (mouseIsPressed === true) { * pg.reset(); * } * } * * @example * let pg; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object. * pg = createGraphics(60, 60); * * describe('A white circle at the center of a dark gray square. The image is drawn on a light gray background.'); * } * * function draw() { * background(200); * * // Translate the p5.Graphics object's coordinate system. * pg.translate(30, 30); * * // Draw to the p5.Graphics object. * pg.background(100); * pg.circle(0, 0, 10); * * // Display the p5.Graphics object. * image(pg, 20, 20); * * // Reset the p5.Graphics object automatically. * pg.reset(); * } * * @example * let pg; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object using WebGL mode. * pg = createGraphics(100, 100, WEBGL); * * describe("A sphere lit from above with a red light. The sphere's surface becomes glossy while the user clicks and holds the mouse."); * } * * function draw() { * background(200); * * // Add a red point light from the top-right. * pg.pointLight(255, 0, 0, 50, -100, 50); * * // Style the sphere. * // It should appear glossy when the * // lighting values are reset. * pg.noStroke(); * pg.specularMaterial(255); * pg.shininess(100); * * // Draw the sphere. * pg.sphere(30); * * // Display the p5.Graphics object. * image(pg, -50, -50); * * // Reset the p5.Graphics object when * // the user presses the mouse. * if (mouseIsPressed === true) { * pg.reset(); * } * } * * @example * let pg; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object using WebGL mode. * pg = createGraphics(100, 100, WEBGL); * * describe('A sphere with a glossy surface is lit from the top-right by a red light.'); * } * * function draw() { * background(200); * * // Add a red point light from the top-right. * pg.pointLight(255, 0, 0, 50, -100, 50); * * // Style the sphere. * pg.noStroke(); * pg.specularMaterial(255); * pg.shininess(100); * * // Draw the sphere. * pg.sphere(30); * * // Display the p5.Graphics object. * image(pg, 0, 0); * * // Reset the p5.Graphics object automatically. * pg.reset(); * } */ reset() { this._renderer.resetMatrix(); if (this._renderer.isP3D) { this._renderer._update(); } } /** * Removes the graphics buffer from the web page. * * Calling `myGraphics.remove()` removes the graphics buffer's * `<canvas>` element from the web page. The graphics buffer also uses * a bit of memory on the CPU that can be freed like so: * * ```js * // Remove the graphics buffer from the web page. * myGraphics.remove(); * * // Delete the graphics buffer from CPU memory. * myGraphics = undefined; * ``` * * Note: All variables that reference the graphics buffer must be assigned * the value `undefined` to delete the graphics buffer from CPU memory. If any * variable still refers to the graphics buffer, then it won't be garbage * collected. * * @example * // Double-click to remove the p5.Graphics object. * * let pg; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object. * pg = createGraphics(60, 60); * * // Draw to the p5.Graphics object. * pg.background(100); * pg.circle(30, 30, 20); * * describe('A white circle at the center of a dark gray square disappears when the user double-clicks.'); * } * * function draw() { * background(200); * * // Display the p5.Graphics object if * // it's available. * if (pg) { * image(pg, 20, 20); * } * } * * // Remove the p5.Graphics object when the * // the user double-clicks. * function doubleClicked() { * // Remove the p5.Graphics object from the web page. * pg.remove(); * pg = undefined; * } */ remove() { this._renderer.remove(); this._renderer = undefined; } /** * Creates a new p5.Framebuffer object with * the same WebGL context as the graphics buffer. * * p5.Framebuffer objects are separate drawing * surfaces that can be used as textures in WebGL mode. They're similar to * p5.Graphics objects and generally run much * faster when used as textures. Creating a * p5.Framebuffer object in the same context * as the graphics buffer makes this speedup possible. * * The parameter, `options`, is optional. An object can be passed to configure * the p5.Framebuffer object. The available * properties are: * * - `format`: data format of the texture, either `UNSIGNED_BYTE`, `FLOAT`, or `HALF_FLOAT`. Default is `UNSIGNED_BYTE`. * - `channels`: whether to store `RGB` or `RGBA` color channels. Default is to match the graphics buffer which is `RGBA`. * - `depth`: whether to include a depth buffer. Default is `true`. * - `depthFormat`: data format of depth information, either `UNSIGNED_INT` or `FLOAT`. Default is `FLOAT`. * - `stencil`: whether to include a stencil buffer for masking. `depth` must be `true` for this feature to work. Defaults to the value of `depth` which is `true`. * - `antialias`: whether to perform anti-aliasing. If set to `true`, as in `{ antialias: true }`, 2 samples will be used by default. The number of samples can also be set, as in `{ antialias: 4 }`. Default is to match setAttributes() which is `false` (`true` in Safari). * - `width`: width of the p5.Framebuffer object. Default is to always match the graphics buffer width. * - `height`: height of the p5.Framebuffer object. Default is to always match the graphics buffer height. * - `density`: pixel density of the p5.Framebuffer object. Default is to always match the graphics buffer pixel density. * - `textureFiltering`: how to read values from the p5.Framebuffer object. Either `LINEAR` (nearby pixels will be interpolated) or `NEAREST` (no interpolation). Generally, use `LINEAR` when using the texture as an image and `NEAREST` if reading the texture as data. Default is `LINEAR`. * * If the `width`, `height`, or `density` attributes are set, they won't * automatically match the graphics buffer and must be changed manually. * * @param {Object} [options] configuration options. * @param {UNSIGNED_BYTE|FLOAT|HALF_FLOAT} [options.format=UNSIGNED_BYTE] The data format of the texture. * @param {RGB|RGBA} [options.channels=RGBA] What color channels to include in the texture. * @param {Boolean} [options.depth=true] Whether to store depth information in the framebuffer. * @param {UNSIGNED_INT|FLOAT} [options.depthFormat=FLOAT] The format to store depth values in. * @param {Boolean} [options.stencil=true] Whether to include a stencil buffer (required for clipping.) * @param {Boolean|Number} [options.antialias] Whether to antialias when drawing to this framebuffer. Either a boolean, or the number of antialias samples to use. * @param {Number} [options.width] The width of the framebuffer. By default, it will match the main canvas. * @param {Number} [options.height] The height of the framebuffer. By default, it will match the main canvas. * @param {Number} [options.density] The pixel density of the framebuffer. By default, it will match the main canvas. * @param {LINEAR|NEAREST} [options.textureFiltering=LINEAR] The strategy used when reading values in the framebuffer in between pixels. * @return {p5.Framebuffer} new framebuffer. * * @example * // Click and hold a mouse button to change shapes. * * let pg; * let torusLayer; * let boxLayer; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object using WebGL mode. * pg = createGraphics(100, 100, WEBGL); * * // Create the p5.Framebuffer objects. * torusLayer = pg.createFramebuffer(); * boxLayer = pg.createFramebuffer(); * * describe('A grid of white toruses rotating against a dark gray background. The shapes become boxes while the user holds a mouse button.'); * } * * function draw() { * // Update and draw the layers offscreen. * drawTorus(); * drawBox(); * * // Choose the layer to display. * let layer; * if (mouseIsPressed === true) { * layer = boxLayer; * } else { * layer = torusLayer; * } * * // Draw to the p5.Graphics object. * pg.background(50); * * // Iterate from left to right. * for (let x = -50; x < 50; x += 25) { * // Iterate from top to bottom. * for (let y = -50; y < 50; y += 25) { * // Draw the layer to the p5.Graphics object * pg.image(layer, x, y, 25, 25); * } * } * * // Display the p5.Graphics object. * image(pg, 0, 0); * } * * // Update and draw the torus layer offscreen. * function drawTorus() { * // Start drawing to the torus p5.Framebuffer. * torusLayer.begin(); * * // Clear the drawing surface. * pg.clear(); * * // Turn on the lights. * pg.lights(); * * // Rotate the coordinate system. * pg.rotateX(frameCount * 0.01); * pg.rotateY(frameCount * 0.01); * * // Style the torus. * pg.noStroke(); * * // Draw the torus. * pg.torus(20); * * // Start drawing to the torus p5.Framebuffer. * torusLayer.end(); * } * * // Update and draw the box layer offscreen. * function drawBox() { * // Start drawing to the box p5.Framebuffer. * boxLayer.begin(); * * // Clear the drawing surface. * pg.clear(); * * // Turn on the lights. * pg.lights(); * * // Rotate the coordinate system. * pg.rotateX(frameCount * 0.01); * pg.rotateY(frameCount * 0.01); * * // Style the box. * pg.noStroke(); * * // Draw the box. * pg.box(30); * * // Start drawing to the box p5.Framebuffer. * boxLayer.end(); * } * * @example * // Click and hold a mouse button to change shapes. * * let pg; * let torusLayer; * let boxLayer; * * function setup() { * createCanvas(100, 100); * * // Create an options object. * let options = { width: 25, height: 25 }; * * // Create a p5.Graphics object using WebGL mode. * pg = createGraphics(100, 100, WEBGL); * * // Create the p5.Framebuffer objects. * // Use options for configuration. * torusLayer = pg.createFramebuffer(options); * boxLayer = pg.createFramebuffer(options); * * describe('A grid of white toruses rotating against a dark gray background. The shapes become boxes while the user holds a mouse button.'); * } * * function draw() { * // Update and draw the layers offscreen. * drawTorus(); * drawBox(); * * // Choose the layer to display. * let layer; * if (mouseIsPressed === true) { * layer = boxLayer; * } else { * layer = torusLayer; * } * * // Draw to the p5.Graphics object. * pg.background(50); * * // Iterate from left to right. * for (let x = -50; x < 50; x += 25) { * // Iterate from top to bottom. * for (let y = -50; y < 50; y += 25) { * // Draw the layer to the p5.Graphics object * pg.image(layer, x, y); * } * } * * // Display the p5.Graphics object. * image(pg, 0, 0); * } * * // Update and draw the torus layer offscreen. * function drawTorus() { * // Start drawing to the torus p5.Framebuffer. * torusLayer.begin(); * * // Clear the drawing surface. * pg.clear(); * * // Turn on the lights. * pg.lights(); * * // Rotate the coordinate system. * pg.rotateX(frameCount * 0.01); * pg.rotateY(frameCount * 0.01); * * // Style the torus. * pg.noStroke(); * * // Draw the torus. * pg.torus(5, 2.5); * * // Start drawing to the torus p5.Framebuffer. * torusLayer.end(); * } * * // Update and draw the box layer offscreen. * function drawBox() { * // Start drawing to the box p5.Framebuffer. * boxLayer.begin(); * * // Clear the drawing surface. * pg.clear(); * * // Turn on the lights. * pg.lights(); * * // Rotate the coordinate system. * pg.rotateX(frameCount * 0.01); * pg.rotateY(frameCount * 0.01); * * // Style the box. * pg.noStroke(); * * // Draw the box. * pg.box(7.5); * * // Start drawing to the box p5.Framebuffer. * boxLayer.end(); * } */ createFramebuffer(options) { return new Framebuffer$1(this._renderer, options); } _assert3d(name) { if (!this._renderer.isP3D) throw new Error( `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` ); }; _initializeInstanceVariables() { this._accessibleOutputs = { text: false, grid: false, textLabel: false, gridLabel: false }; this._styles = []; // this._colorMode = RGB; // this._colorMaxes = { // rgb: [255, 255, 255, 255], // hsb: [360, 100, 100, 1], // hsl: [360, 100, 100, 1] // }; this._downKeys = {}; //Holds the key codes of currently pressed keys } } function graphics(p5, fn){ /** * A class to describe a drawing surface that's separate from the main canvas. * * Each `p5.Graphics` object provides a dedicated drawing surface called a * *graphics buffer*. Graphics buffers are helpful when drawing should happen * offscreen. For example, separate scenes can be drawn offscreen and * displayed only when needed. * * `p5.Graphics` objects have nearly all the drawing features of the main * canvas. For example, calling the method `myGraphics.circle(50, 50, 20)` * draws to the graphics buffer. The resulting image can be displayed on the * main canvas by passing the `p5.Graphics` object to the * image() function, as in `image(myGraphics, 0, 0)`. * * Note: createGraphics() is the recommended * way to create an instance of this class. * * @class p5.Graphics * @extends p5.Element * @param {Number} w width width of the graphics buffer in pixels. * @param {Number} h height height of the graphics buffer in pixels. * @param {(P2D|WEBGL|P2DHDR)} renderer the renderer to use, either P2D or WEBGL. * @param {p5} [pInst] sketch instance. * @param {HTMLCanvasElement} [canvas] existing `<canvas>` element to use. * * @example * let pg; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object. * pg = createGraphics(50, 50); * * // Draw to the p5.Graphics object. * pg.background(100); * pg.circle(25, 25, 20); * * describe('A dark gray square with a white circle at its center drawn on a gray background.'); * } * * function draw() { * background(200); * * // Display the p5.Graphics object. * image(pg, 25, 25); * } * * @example * // Click the canvas to display the graphics buffer. * * let pg; * * function setup() { * createCanvas(100, 100); * * // Create a p5.Graphics object. * pg = createGraphics(50, 50); * * describe('A square appears on a gray background when the user presses the mouse. The square cycles between white and black.'); * } * * function draw() { * background(200); * * // Calculate the background color. * let bg = frameCount % 255; * * // Draw to the p5.Graphics object. * pg.background(bg); * * // Display the p5.Graphics object while * // the user presses the mouse. * if (mouseIsPressed === true) { * image(pg, 25, 25); * } * } */ p5.Graphics = Graphics; // Shapes primitives(p5, p5.Graphics.prototype); attributes(p5, p5.Graphics.prototype); curves(p5, p5.Graphics.prototype); vertex(p5, p5.Graphics.prototype); customShapes(p5, p5.Graphics.prototype); setting(p5, p5.Graphics.prototype); loadingDisplaying(p5, p5.Graphics.prototype); image$1(p5, p5.Graphics.prototype); pixels(p5, p5.Graphics.prototype); transform$1(p5, p5.Graphics.prototype); primitives3D(p5, p5.Graphics.prototype); light(p5, p5.Graphics.prototype); material(p5, p5.Graphics.prototype); creatingReading(p5, p5.Graphics.prototype); trigonometry(p5, p5.Graphics.prototype); } /** * This module defines the p5.Texture class * @module 3D * @submodule Material * @for p5 * @requires core */ class Texture { constructor (renderer, obj, settings = {}) { this._renderer = renderer; this.src = obj; this.format = settings.format || 'rgba8unorm'; this.minFilter = settings.minFilter || LINEAR; this.magFilter = settings.magFilter || LINEAR; this.wrapS = settings.wrapS || renderer.states.textureWrapX; this.wrapT = settings.wrapT || renderer.states.textureWrapY; this.dataType = settings.dataType || 'uint8'; this.textureHandle = null; this._detectSourceType(); const textureData = this._getTextureDataFromSource(); this.width = textureData.width; this.height = textureData.height; this.init(textureData); } /* const support = checkWebGLCapabilities(renderer); if (this.glFormat === gl.HALF_FLOAT && !support.halfFloat) { console.log('This device does not support dataType HALF_FLOAT. Falling back to FLOAT.'); this.glDataType = gl.FLOAT; } if ( this.glFormat === gl.HALF_FLOAT && (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) && !support.halfFloatLinear ) { console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.'); if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST; if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST; } if (this.glFormat === gl.FLOAT && !support.float) { console.log('This device does not support dataType FLOAT. Falling back to UNSIGNED_BYTE.'); this.glDataType = gl.UNSIGNED_BYTE; } if ( this.glFormat === gl.FLOAT && (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) && !support.floatLinear ) { console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.'); if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST; if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST; } }*/ _detectSourceType() { const obj = this.src; this.isFramebufferTexture = obj instanceof FramebufferTexture; this.isSrcP5Image = obj instanceof Image; this.isSrcP5Graphics = obj instanceof Graphics; this.isSrcP5Renderer = obj instanceof Renderer; this.isImageData = typeof ImageData !== 'undefined' && obj instanceof ImageData; this.isSrcMediaElement = typeof MediaElement !== 'undefined' && obj instanceof MediaElement; this.isSrcHTMLElement = typeof Element !== 'undefined' && obj instanceof Element && !this.isSrcMediaElement && !this.isSrcP5Graphics && !this.isSrcP5Renderer; } remove() { if (this.textureHandle) { this._renderer.deleteTexture(this.textureHandle); this.textureHandle = null; } } _getTextureDataFromSource () { let textureData; if (this.isFramebufferTexture) { textureData = this.src.rawTexture(); } else if (this.isSrcP5Image) { // param is a p5.Image textureData = this.src.canvas; } else if ( this.isSrcMediaElement || this.isSrcHTMLElement ) { // if param is a video HTML element if (this.src._ensureCanvas) { this.src._ensureCanvas(); } textureData = this.src.elt; } else if (this.isSrcP5Graphics || this.isSrcP5Renderer) { textureData = this.src.canvas; } else if (this.isImageData) { textureData = this.src; } return textureData; } /** * Initializes common texture parameters, creates a gl texture, * tries to upload the texture for the first time if data is * already available. */ init(textureData) { if (!this.isFramebufferTexture) { this.textureHandle = this._renderer.createTexture({ format: this.format, dataType: this.dataType, width: textureData.width, height: textureData.height, }); } else { this.textureHandle = this._renderer.createFramebufferTextureHandle(this.src); } this._renderer.setTextureParams(this, { minFilter: this.minFilter, magFilter: this.magFilter, wrapS: this.wrapS, wrapT: this.wrapT }); this.bindTexture(); if (this._shouldDeferUpload()) { this._renderer.uploadTextureFromData( this.textureHandle, new Uint8Array(1, 1, 1, 1), 1, 1 ); } else if (!this.isFramebufferTexture) { // this.update() this._renderer.uploadTextureFromSource( this.textureHandle, textureData ); } this.unbindTexture(); } _shouldDeferUpload() { return ( this.width === 0 || this.height === 0 || (this.isSrcMediaElement && !this.src.loadedmetadata) ); } /** * Checks if the source data for this texture has changed (if it's * easy to do so) and reuploads the texture if necessary. If it's not * possible or to expensive to do a calculation to determine wheter or * not the data has occurred, this method simply re-uploads the texture. */ update() { const textureData = this._getTextureDataFromSource(); if (!textureData) return false; let updated = false; if (this._shouldUpdate(textureData)) { this.bindTexture(); this._renderer.uploadTextureFromSource(this.textureHandle, textureData); updated = true; } return updated; } _shouldUpdate(textureData) { const data = this.src; if (data.width === 0 || data.height === 0) { return false; // nothing to do! } // FramebufferTexture instances wrap raw WebGL textures already, which // don't need any extra updating, as they already live on the GPU if (this.isFramebufferTexture) { this.src.update(); return false; } let updated = false; // pull texture from data, make sure width & height are appropriate if ( textureData.width !== this.width || textureData.height !== this.height ) { updated = true; // make sure that if the width and height of this.src have changed // for some reason, we update our metadata and upload the texture again this.width = textureData.width || data.width; this.height = textureData.height || data.height; if (this.isSrcP5Image) { data.setModified(false); } else if (this.isSrcMediaElement || this.isSrcHTMLElement) { // on the first frame the metadata comes in, the size will be changed // from 0 to actual size, but pixels may not be available. // flag for update in a future frame. // if we don't do this, a paused video, for example, may not // send the first frame to texture memory. data.setModified && data.setModified(true); } } else if (this.isSrcP5Image) { if (data.gifProperties) { data._animateGif(this._renderer._pInst); } // for an image, we only update if the modified field has been set, // for example, by a call to p5.Image.set if (data.isModified()) { updated = true; data.setModified(false); } } else if (this.isSrcMediaElement) { // for a media element (video), we'll check if the current time in // the video frame matches the last time. if it doesn't match, the // video has advanced or otherwise been taken to a new frame, // and we need to upload it. if (data.isModified()) { // p5.MediaElement may have also had set/updatePixels, etc. called // on it and should be updated, or may have been set for the first // time! updated = true; data.setModified(false); } else if (data.loadedmetadata) { // if the meta data has been loaded, we can ask the video // what it's current position (in time) is. if (this._videoPrevUpdateTime !== data.time()) { // update the texture in gpu mem only if the current // video timestamp does not match the timestamp of the last // time we uploaded this texture (and update the time we // last uploaded, too) this._videoPrevUpdateTime = data.time(); updated = true; } } } else if (this.isImageData) { if (data._dirty) { data._dirty = false; updated = true; } } else { /* data instanceof p5.Graphics, probably */ // there is not enough information to tell if the texture can be // conditionally updated; so to be safe, we just go ahead and upload it. updated = true; } return updated; } bindTexture() { this._renderer.bindTexture(this); return this; } unbindTexture () { this._renderer.unbindTexture(); } getTexture() { if (this.isFramebufferTexture) { return this.src.rawTexture(); } else { return this.textureHandle; } } getSampler() { return this._renderer.getSampler(this); } setInterpolation(minFilter, magFilter) { this.minFilter = minFilter; this.magFilter = magFilter; this._renderer.setTextureParams(this); } setWrapMode(wrapX, wrapY) { this.wrapS = wrapX; this.wrapT = wrapY; this._renderer.setTextureParams(this); } } class MipmapTexture extends Texture { constructor(renderer, levels, settings = {}) { // Set default mipmap filtering const mipmapSettings = { minFilter: LINEAR, magFilter: LINEAR, ...settings }; super(renderer, levels, mipmapSettings); this.levels = levels; } _getTextureDataFromSource() { return this.src; } init(levels) { // Handle both ImageData array (WebGL) and WebGPU texture object if (Array.isArray(levels)) { // WebGL path: levels is array of ImageData const firstLevel = levels[0]; this.width = firstLevel.width; this.height = firstLevel.height; // Let renderer create the mipmap texture handle this.textureHandle = this._renderer.createMipmapTextureHandle({ levels: levels, format: this.format, dataType: this.dataType, width: this.width, height: this.height, }); } else { // WebGPU path: levels is a mipmapData object with pre-built GPU texture this.width = levels.size; this.height = levels.size; // Let renderer create the texture handle from the GPU texture this.textureHandle = this._renderer.createMipmapTextureHandle({ gpuTexture: levels.gpuTexture, format: levels.format, dataType: 'uint8', width: this.width, height: this.height, }); } this._renderer.setTextureParams(this, { minFilter: this.minFilter, magFilter: this.magFilter, wrapS: this.wrapS, wrapT: this.wrapT }); } update() {} } function texture(p5, fn){ /** * Texture class for WEBGL Mode * @private * @class p5.Texture * @param {p5.RendererGL} renderer an instance of p5.RendererGL that * will provide the GL context for this new p5.Texture * @param {p5.Image|p5.Graphics|p5.Element|p5.MediaElement|ImageData|p5.Framebuffer|p5.FramebufferTexture|ImageData} [obj] the * object containing the image data to store in the texture. * @param {Object} [settings] optional A javascript object containing texture * settings. * @param {Number} [settings.format] optional The internal color component * format for the texture. Possible values for format include gl.RGBA, * gl.RGB, gl.ALPHA, gl.LUMINANCE, gl.LUMINANCE_ALPHA. Defaults to gl.RBGA * @param {Number} [settings.minFilter] optional The texture minification * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults * to gl.LINEAR. Note, Mipmaps are not implemented in p5. * @param {Number} [settings.magFilter] optional The texture magnification * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults * to gl.LINEAR. Note, Mipmaps are not implemented in p5. * @param {Number} [settings.wrapS] optional The texture wrap settings for * the s coordinate, or x axis. Possible values are gl.CLAMP_TO_EDGE, * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE * @param {Number} [settings.wrapT] optional The texture wrap settings for * the t coordinate, or y axis. Possible values are gl.CLAMP_TO_EDGE, * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE * @param {Number} [settings.dataType] optional The data type of the texel * data. Possible values are gl.UNSIGNED_BYTE or gl.FLOAT. There are more * formats that are not implemented in p5. Defaults to gl.UNSIGNED_BYTE. */ p5.Texture = Texture; p5.MipmapTexture = MipmapTexture; } if(typeof p5 !== 'undefined'){ texture(p5); } /** * @private * @param {Uint8Array|Float32Array|undefined} pixels An existing pixels array to reuse if the size is the same * @param {WebGLRenderingContext} gl The WebGL context * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read * @param {Number} x The x coordiante to read, premultiplied by pixel density * @param {Number} y The y coordiante to read, premultiplied by pixel density * @param {Number} width The width in pixels to be read (factoring in pixel density) * @param {Number} height The height in pixels to be read (factoring in pixel density) * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about * @returns {Uint8Array|Float32Array} pixels A pixels array with the current state of the * WebGL context read into it */ function readPixelsWebGL( pixels, gl, framebuffer, x, y, width, height, format, type, flipY, ) { // Record the currently bound framebuffer so we can go back to it after, and // bind the framebuffer we want to read from const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); const channels = format === gl.RGBA ? 4 : 3; // Make a pixels buffer if it doesn't already exist const len = width * height * channels; const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; if (!(pixels instanceof TypedArrayClass) || pixels.length !== len) { pixels = new TypedArrayClass(len); } gl.readPixels( x, flipY ? flipY - y - height : y, width, height, format, type, pixels, ); // Re-bind whatever was previously bound gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); if (flipY) { // WebGL pixels are inverted compared to 2D pixels, so we have to flip // the resulting rows. Adapted from https://stackoverflow.com/a/41973289 const halfHeight = Math.floor(height / 2); const tmpRow = new TypedArrayClass(width * channels); for (let y = 0; y < halfHeight; y++) { const topOffset = y * width * 4; const bottomOffset = (height - y - 1) * width * 4; tmpRow.set(pixels.subarray(topOffset, topOffset + width * 4)); pixels.copyWithin(topOffset, bottomOffset, bottomOffset + width * 4); pixels.set(tmpRow, bottomOffset); } } return pixels; } /** * @private * @param {WebGLRenderingContext} gl The WebGL context * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read * @param {Number} x The x coordinate to read, premultiplied by pixel density * @param {Number} y The y coordinate to read, premultiplied by pixel density * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about * @returns {Number[]} pixels The channel data for the pixel at that location */ function readPixelWebGL(gl, framebuffer, x, y, format, type, flipY) { // Record the currently bound framebuffer so we can go back to it after, and // bind the framebuffer we want to read from const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); const channels = format === gl.RGBA ? 4 : 3; const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; const pixels = new TypedArrayClass(channels); gl.readPixels(x, flipY ? flipY - y - 1 : y, 1, 1, format, type, pixels); // Re-bind whatever was previously bound gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); return Array.from(pixels); } function setWebGLTextureParams(texture, gl, webglVersion) { texture.bindTexture(); const glMinFilter = texture.minFilter === NEAREST ? gl.NEAREST : texture.minFilter === LINEAR_MIPMAP ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR; const glMagFilter = texture.magFilter === NEAREST ? gl.NEAREST : gl.LINEAR; // for webgl 1 we need to check if the texture is power of two // if it isn't we will set the wrap mode to CLAMP // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet const isPowerOfTwo = (x) => (x & (x - 1)) === 0; const textureData = texture._getTextureDataFromSource(); let wrapWidth; let wrapHeight; if (textureData.naturalWidth && textureData.naturalHeight) { wrapWidth = textureData.naturalWidth; wrapHeight = textureData.naturalHeight; } else { wrapWidth = texture.width; wrapHeight = texture.height; } const widthPowerOfTwo = isPowerOfTwo(wrapWidth); const heightPowerOfTwo = isPowerOfTwo(wrapHeight); let glWrapS, glWrapT; if (texture.wrapS === REPEAT) { if ( webglVersion === WEBGL2 || (widthPowerOfTwo && heightPowerOfTwo) ) { glWrapS = gl.REPEAT; } else { console.warn( "You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapS = gl.CLAMP_TO_EDGE; } } else if (texture.wrapS === MIRROR) { if ( webglVersion === WEBGL2 || (widthPowerOfTwo && heightPowerOfTwo) ) { glWrapS = gl.MIRRORED_REPEAT; } else { console.warn( "You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapS = gl.CLAMP_TO_EDGE; } } else { // falling back to default if didn't get a proper mode glWrapS = gl.CLAMP_TO_EDGE; } if (texture.wrapT === REPEAT) { if ( webglVersion === WEBGL2 || (widthPowerOfTwo && heightPowerOfTwo) ) { glWrapT = gl.REPEAT; } else { console.warn( "You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapT = gl.CLAMP_TO_EDGE; } } else if (texture.wrapT === MIRROR) { if ( webglVersion === WEBGL2 || (widthPowerOfTwo && heightPowerOfTwo) ) { glWrapT = gl.MIRRORED_REPEAT; } else { console.warn( "You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapT = gl.CLAMP_TO_EDGE; } } else { // falling back to default if didn't get a proper mode glWrapT = gl.CLAMP_TO_EDGE; } gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, glMinFilter); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, glMagFilter); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, glWrapS); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, glWrapT); texture.unbindTexture(); } function setWebGLUniformValue(shader, uniform, data, getTexture, gl) { const location = uniform.location; shader.useProgram(); switch (uniform.type) { case gl.BOOL: if (data === true) { gl.uniform1i(location, 1); } else { gl.uniform1i(location, 0); } break; case gl.INT: if (uniform.size > 1) { data.length && gl.uniform1iv(location, data); } else { gl.uniform1i(location, data); } break; case gl.FLOAT: if (uniform.size > 1) { data.length && gl.uniform1fv(location, data); } else { gl.uniform1f(location, data); } break; case gl.FLOAT_MAT3: gl.uniformMatrix3fv(location, false, data); break; case gl.FLOAT_MAT4: gl.uniformMatrix4fv(location, false, data); break; case gl.FLOAT_VEC2: if (uniform.size > 1) { data.length && gl.uniform2fv(location, data); } else { gl.uniform2f(location, data[0], data[1]); } break; case gl.FLOAT_VEC3: if (uniform.size > 1) { data.length && gl.uniform3fv(location, data); } else { gl.uniform3f(location, data[0], data[1], data[2]); } break; case gl.FLOAT_VEC4: if (uniform.size > 1) { data.length && gl.uniform4fv(location, data); } else { gl.uniform4f(location, data[0], data[1], data[2], data[3]); } break; case gl.INT_VEC2: if (uniform.size > 1) { data.length && gl.uniform2iv(location, data); } else { gl.uniform2i(location, data[0], data[1]); } break; case gl.INT_VEC3: if (uniform.size > 1) { data.length && gl.uniform3iv(location, data); } else { gl.uniform3i(location, data[0], data[1], data[2]); } break; case gl.INT_VEC4: if (uniform.size > 1) { data.length && gl.uniform4iv(location, data); } else { gl.uniform4i(location, data[0], data[1], data[2], data[3]); } break; case gl.SAMPLER_2D: if (typeof data == "number") { if ( data < gl.TEXTURE0 || data > gl.TEXTURE31 || data !== Math.ceil(data) ) { console.log( "🌸 p5.js says: " + "You're trying to use a number as the data for a texture." + "Please use a texture.", ); return; } gl.activeTexture(data); gl.uniform1i(location, data); } else { gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); uniform.texture = data instanceof Texture ? data : getTexture(data); gl.uniform1i(location, uniform.samplerIndex); } break; case gl.SAMPLER_CUBE: case gl.SAMPLER_3D: case gl.SAMPLER_2D_SHADOW: case gl.SAMPLER_2D_ARRAY: case gl.SAMPLER_2D_ARRAY_SHADOW: case gl.SAMPLER_CUBE_SHADOW: case gl.INT_SAMPLER_2D: case gl.INT_SAMPLER_3D: case gl.INT_SAMPLER_CUBE: case gl.INT_SAMPLER_2D_ARRAY: case gl.UNSIGNED_INT_SAMPLER_2D: case gl.UNSIGNED_INT_SAMPLER_3D: case gl.UNSIGNED_INT_SAMPLER_CUBE: case gl.UNSIGNED_INT_SAMPLER_2D_ARRAY: if (typeof data !== "number") { break; } if ( data < gl.TEXTURE0 || data > gl.TEXTURE31 || data !== Math.ceil(data) ) { console.log( "🌸 p5.js says: " + "You're trying to use a number as the data for a texture." + "Please use a texture.", ); break; } gl.activeTexture(data); gl.uniform1i(location, data); break; //@todo complete all types } } function getWebGLUniformMetadata(shader, gl) { const program = shader._glProgram; const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); const result = []; let samplerIndex = 0; for (let i = 0; i < numUniforms; ++i) { const uniformInfo = gl.getActiveUniform(program, i); const uniform = {}; uniform.location = gl.getUniformLocation(program, uniformInfo.name); uniform.size = uniformInfo.size; let uniformName = uniformInfo.name; //uniforms that are arrays have their name returned as //someUniform[0] which is a bit silly so we trim it //off here. The size property tells us that its an array //so we dont lose any information by doing this if (uniformInfo.size > 1) { uniformName = uniformName.substring(0, uniformName.indexOf("[0]")); } uniform.name = uniformName; uniform.type = uniformInfo.type; uniform._cachedData = undefined; if (uniform.type === gl.SAMPLER_2D) { uniform.isSampler = true; uniform.samplerIndex = samplerIndex; samplerIndex++; } uniform.isArray = uniformInfo.size > 1 || uniform.type === gl.FLOAT_MAT3 || uniform.type === gl.FLOAT_MAT4 || uniform.type === gl.FLOAT_VEC2 || uniform.type === gl.FLOAT_VEC3 || uniform.type === gl.FLOAT_VEC4 || uniform.type === gl.INT_VEC2 || uniform.type === gl.INT_VEC4 || uniform.type === gl.INT_VEC3; result.push(uniform); } return result; } function getWebGLShaderAttributes(shader, gl) { const attributes = {}; const numAttributes = gl.getProgramParameter( shader._glProgram, gl.ACTIVE_ATTRIBUTES, ); for (let i = 0; i < numAttributes; ++i) { const attributeInfo = gl.getActiveAttrib(shader._glProgram, i); const name = attributeInfo.name; const location = gl.getAttribLocation(shader._glProgram, name); const attribute = {}; attribute.name = name; attribute.location = location; attribute.index = i; attribute.type = attributeInfo.type; attribute.size = attributeInfo.size; attributes[name] = attribute; } return attributes; } function populateGLSLHooks(shader, src, shaderType) { const main = "void main"; if (!src.includes(main)) return src; let [preMain, postMain] = src.split(main); let hooks = ""; let defines = ""; for (const key in shader.hooks.uniforms) { hooks += `uniform ${key};\n`; } if (shader.hooks.declarations) { hooks += shader.hooks.declarations + "\n"; } if (shader.hooks[shaderType].declarations) { hooks += shader.hooks[shaderType].declarations + "\n"; } // Handle varying variables from p5.strands if ( shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0 ) { for (const varyingVar of shader.hooks.varyingVariables) { // Generate OUT declaration for vertex shader, IN declaration for fragment shader if (shaderType === "vertex") { hooks += `OUT ${varyingVar};\n`; } else if (shaderType === "fragment") { hooks += `IN ${varyingVar};\n`; } } } for (const hookDef in shader.hooks.helpers) { hooks += `${hookDef}${shader.hooks.helpers[hookDef]}\n`; } for (const hookDef in shader.hooks[shaderType]) { if (hookDef === "declarations") continue; const [hookType, hookName] = hookDef.split(" "); // Add a #define so that if the shader wants to use preprocessor directives to // optimize away the extra function calls in main, it can do so if ( shader.hooks.modified.vertex[hookDef] || shader.hooks.modified.fragment[hookDef] ) { defines += "#define AUGMENTED_HOOK_" + hookName + "\n"; } hooks += hookType + " HOOK_" + hookName + shader.hooks[shaderType][hookDef] + "\n"; } // Allow shaders to specify the location of hook #define statements. Normally these // go after function definitions, but one might want to have them defined earlier // in order to only conditionally make uniforms. if (preMain.indexOf("#define HOOK_DEFINES") !== -1) { preMain = preMain.replace("#define HOOK_DEFINES", "\n" + defines + "\n"); defines = ""; } return preMain + "\n" + defines + hooks + main + postMain; } function checkWebGLCapabilities({ GL, webglVersion }) { const gl = GL; const supportsFloat = webglVersion === WEBGL2 ? gl.getExtension("EXT_color_buffer_float") && gl.getExtension("EXT_float_blend") : gl.getExtension("OES_texture_float"); const supportsFloatLinear = supportsFloat && gl.getExtension("OES_texture_float_linear"); const supportsHalfFloat = webglVersion === WEBGL2 ? gl.getExtension("EXT_color_buffer_float") : gl.getExtension("OES_texture_half_float"); const supportsHalfFloatLinear = supportsHalfFloat && gl.getExtension("OES_texture_half_float_linear"); return { float: supportsFloat, floatLinear: supportsFloatLinear, halfFloat: supportsHalfFloat, halfFloatLinear: supportsHalfFloatLinear, }; } /** * @module Rendering * @requires constants */ const constrain = (n, low, high) => Math.max(Math.min(n, high), low); class FramebufferCamera extends Camera { constructor(framebuffer) { super(framebuffer.renderer); this.fbo = framebuffer; this.yScale = framebuffer.renderer.framebufferYScale(); } _computeCameraDefaultSettings() { super._computeCameraDefaultSettings(); this.defaultAspectRatio = this.fbo.width / this.fbo.height; this.defaultCameraFOV = 2 * Math.atan(this.fbo.height / 2 / this.defaultEyeZ); } copy() { const _cam = super.copy(); _cam.fbo = this.fbo; return _cam; } } class FramebufferTexture { constructor(framebuffer, property) { this.framebuffer = framebuffer; this.property = property; } get width() { return this.framebuffer.width * this.framebuffer.density; } get height() { return this.framebuffer.height * this.framebuffer.density; } update() { this.framebuffer._update(this.property); } rawTexture() { return { texture: this.framebuffer[this.property] }; } } let Framebuffer$1 = class Framebuffer { constructor(renderer, settings = {}) { this.renderer = renderer; this.renderer.framebuffers.add(this); this._isClipApplied = false; this._useCanvasFormat = settings._useCanvasFormat || false; this.dirty = { colorTexture: false, depthTexture: false }; this.pixels = []; this.format = settings.format || UNSIGNED_BYTE; this.channels = settings.channels || ( this.renderer.defaultFramebufferAlpha() ? RGBA : RGB ); this.useDepth = settings.depth === undefined ? true : settings.depth; this.depthFormat = settings.depthFormat || FLOAT; this.textureFiltering = settings.textureFiltering || LINEAR; if (settings.antialias === undefined) { this.antialiasSamples = this.renderer.defaultFramebufferAntialias() ? 2 : 0; } else if (typeof settings.antialias === 'number') { this.antialiasSamples = settings.antialias; } else { this.antialiasSamples = settings.antialias ? 2 : 0; } this.antialias = this.antialiasSamples > 0; if (this.antialias && !this.renderer.supportsFramebufferAntialias()) { console.warn('Framebuffer antialiasing is unsupported in this context'); this.antialias = false; } this.density = settings.density || this.renderer._pixelDensity; if (settings.width && settings.height) { const dimensions = this.renderer._adjustDimensions(settings.width, settings.height); this.width = dimensions.adjustedWidth; this.height = dimensions.adjustedHeight; this._autoSized = false; } else { if ((settings.width === undefined) !== (settings.height === undefined)) { console.warn( 'Please supply both width and height for a framebuffer to give it a ' + 'size. Only one was given, so the framebuffer will match the size ' + 'of its canvas.' ); } this.width = this.renderer.width; this.height = this.renderer.height; this._autoSized = true; } // Let renderer validate and adjust formats for this context this.renderer.validateFramebufferFormats(this); if (settings.stencil && !this.useDepth) { console.warn('A stencil buffer can only be used if also using depth. Since the framebuffer has no depth buffer, the stencil buffer will be ignored.'); } this.useStencil = this.useDepth && (settings.stencil === undefined ? true : settings.stencil); // Let renderer create framebuffer resources with antialiasing support this.renderer.createFramebufferResources(this); this._recreateTextures(); this.defaultCamera = this.createCamera(); this.filterCamera = this.createCamera(); this.draw(() => this.renderer.clear()); } /** * Resizes the framebuffer to a given width and height. * * The parameters, `width` and `height`, set the dimensions of the * framebuffer. For example, calling `myBuffer.resize(300, 500)` resizes * the framebuffer to 300×500 pixels, then sets `myBuffer.width` to 300 * and `myBuffer.height` 500. * * @param {Number} width width of the framebuffer. * @param {Number} height height of the framebuffer. * * @example * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * describe('A multicolor sphere on a white surface. The image grows larger or smaller when the user moves the mouse, revealing a gray background.'); * } * * function draw() { * background(200); * * // Draw to the p5.Framebuffer object. * myBuffer.begin(); * background(255); * normalMaterial(); * sphere(20); * myBuffer.end(); * * // Display the p5.Framebuffer object. * image(myBuffer, -50, -50); * } * * // Resize the p5.Framebuffer object when the * // user moves the mouse. * function mouseMoved() { * myBuffer.resize(mouseX, mouseY); * } */ resize(width, height) { this._autoSized = false; const dimensions = this.renderer._adjustDimensions(width, height); width = dimensions.adjustedWidth; height = dimensions.adjustedHeight; this.width = width; this.height = height; this._handleResize(); } /** * Sets the framebuffer's pixel density or returns its current density. * * Computer displays are grids of little lights called pixels. A display's * pixel density describes how many pixels it packs into an area. Displays * with smaller pixels have a higher pixel density and create sharper * images. * * The parameter, `density`, is optional. If a number is passed, as in * `myBuffer.pixelDensity(1)`, it sets the framebuffer's pixel density. By * default, the framebuffer's pixel density will match that of the canvas * where it was created. All canvases default to match the display's pixel * density. * * Calling `myBuffer.pixelDensity()` without an argument returns its current * pixel density. * * @param {Number} [density] pixel density to set. * @returns {Number} current pixel density. * * @example * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * describe("A white circle on a gray canvas. The circle's edge become fuzzy while the user presses and holds the mouse."); * } * * function draw() { * // Draw to the p5.Framebuffer object. * myBuffer.begin(); * background(200); * circle(0, 0, 40); * myBuffer.end(); * * // Display the p5.Framebuffer object. * image(myBuffer, -50, -50); * } * * // Decrease the pixel density when the user * // presses the mouse. * function mousePressed() { * myBuffer.pixelDensity(1); * } * * // Increase the pixel density when the user * // releases the mouse. * function mouseReleased() { * myBuffer.pixelDensity(2); * } * * @example * let myBuffer; * let myFont; * * async function setup() { * // Load a font and create a p5.Font object. * myFont = await loadFont('assets/inconsolata.otf'); * * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * // Get the p5.Framebuffer object's pixel density. * let d = myBuffer.pixelDensity(); * * // Style the text. * textAlign(CENTER, CENTER); * textFont(myFont); * textSize(16); * fill(0); * * // Display the pixel density. * text(`Density: ${d}`, 0, 0); * * describe(`The text "Density: ${d}" written in black on a gray background.`); * } */ pixelDensity(density) { if (density) { this._autoSized = false; this.density = density; this._handleResize(); } else { return this.density; } } /** * Toggles the framebuffer's autosizing mode or returns the current mode. * * By default, the framebuffer automatically resizes to match the canvas * that created it. Calling `myBuffer.autoSized(false)` disables this * behavior and calling `myBuffer.autoSized(true)` re-enables it. * * Calling `myBuffer.autoSized()` without an argument returns `true` if * the framebuffer automatically resizes and `false` if not. * * @param {Boolean} [autoSized] whether to automatically resize the framebuffer to match the canvas. * @returns {Boolean} current autosize setting. * * @example * // Double-click to toggle the autosizing mode. * * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * describe('A multicolor sphere on a gray background. The image resizes when the user moves the mouse.'); * } * * function draw() { * background(50); * * // Draw to the p5.Framebuffer object. * myBuffer.begin(); * background(200); * normalMaterial(); * sphere(width / 4); * myBuffer.end(); * * // Display the p5.Framebuffer object. * image(myBuffer, -width / 2, -height / 2); * } * * // Resize the canvas when the user moves the mouse. * function mouseMoved() { * let w = constrain(mouseX, 0, 100); * let h = constrain(mouseY, 0, 100); * resizeCanvas(w, h); * } * * // Toggle autoSizing when the user double-clicks. * // Note: opened an issue to fix(?) this. * function doubleClicked() { * let isAuto = myBuffer.autoSized(); * myBuffer.autoSized(!isAuto); * } */ autoSized(autoSized) { if (autoSized === undefined) { return this._autoSized; } else { this._autoSized = autoSized; this._handleResize(); } } /** * Checks the capabilities of the current WebGL environment to see if the * settings supplied by the user are capable of being fulfilled. If they * are not, warnings will be logged and the settings will be changed to * something close that can be fulfilled. * * @private */ _checkIfFormatsAvailable() { const gl = this.gl; if ( this.useDepth && this.renderer.webglVersion === WEBGL && !gl.getExtension('WEBGL_depth_texture') ) { console.warn( 'Unable to create depth textures in this environment. Falling back ' + 'to a framebuffer without depth.' ); this.useDepth = false; } if ( this.useDepth && this.renderer.webglVersion === WEBGL && this.depthFormat === FLOAT ) { console.warn( 'FLOAT depth format is unavailable in WebGL 1. ' + 'Defaulting to UNSIGNED_INT.' ); this.depthFormat = UNSIGNED_INT; } if (![ UNSIGNED_BYTE, FLOAT, HALF_FLOAT ].includes(this.format)) { console.warn( 'Unknown Framebuffer format. ' + 'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' + 'Defaulting to UNSIGNED_BYTE.' ); this.format = UNSIGNED_BYTE; } if (this.useDepth && ![ UNSIGNED_INT, FLOAT ].includes(this.depthFormat)) { console.warn( 'Unknown Framebuffer depth format. ' + 'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.' ); this.depthFormat = FLOAT; } const support = checkWebGLCapabilities(this.renderer); if (!support.float && this.format === FLOAT) { console.warn( 'This environment does not support FLOAT textures. ' + 'Falling back to UNSIGNED_BYTE.' ); this.format = UNSIGNED_BYTE; } if ( this.useDepth && !support.float && this.depthFormat === FLOAT ) { console.warn( 'This environment does not support FLOAT depth textures. ' + 'Falling back to UNSIGNED_INT.' ); this.depthFormat = UNSIGNED_INT; } if (!support.halfFloat && this.format === HALF_FLOAT) { console.warn( 'This environment does not support HALF_FLOAT textures. ' + 'Falling back to UNSIGNED_BYTE.' ); this.format = UNSIGNED_BYTE; } if ( this.channels === RGB && [FLOAT, HALF_FLOAT].includes(this.format) ) { console.warn( 'FLOAT and HALF_FLOAT formats do not work cross-platform with only ' + 'RGB channels. Falling back to RGBA.' ); this.channels = RGBA; } } _deleteTextures() { this.renderer.deleteFramebufferTextures(this); } /** * Creates new textures and renderbuffers given the current size of the * framebuffer. * * @private */ _recreateTextures() { this._updateSize(); // Let renderer handle texture creation and framebuffer setup this.renderer.recreateFramebufferTextures(this); if (this.useDepth) { this.depth = new FramebufferTexture(this, 'depthTexture'); const depthFilter = NEAREST; this.depthP5Texture = new Texture( this.renderer, this.depth, { minFilter: depthFilter, magFilter: depthFilter } ); this.renderer.textures.set(this.depth, this.depthP5Texture); } this.color = new FramebufferTexture(this, 'colorTexture'); const filter = this.textureFiltering === LINEAR ? LINEAR : NEAREST; this.colorP5Texture = new Texture( this.renderer, this.color, { minFilter: filter, magFilter: filter } ); this.renderer.textures.set(this.color, this.colorP5Texture); } /** * A method that will be called when recreating textures. If the framebuffer * is auto-sized, it will update its width, height, and density properties. * * @private */ _updateSize() { if (this._autoSized) { this.width = this.renderer.width; this.height = this.renderer.height; this.density = this.renderer._pixelDensity; } } /** * Called when the canvas that the framebuffer is attached to resizes. If the * framebuffer is auto-sized, it will update its textures to match the new * size. * * @private */ _canvasSizeChanged() { if (this._autoSized) { this._handleResize(); } } /** * Called when the size of the framebuffer has changed (either by being * manually updated or from auto-size updates when its canvas changes size.) * Old textures and renderbuffers will be deleted, and then recreated with the * new size. * * @private */ _handleResize() { this._deleteTextures(); this._recreateTextures(); this.defaultCamera._resize(); } /** * Creates a new * p5.Camera object to use with the framebuffer. * * The new camera is initialized with a default position `(0, 0, 800)` and a * default perspective projection. Its properties can be controlled with * p5.Camera methods such as `myCamera.lookAt(0, 0, 0)`. * * Framebuffer cameras should be created between calls to * myBuffer.begin() and * myBuffer.end() like so: * * ```js * let myCamera; * * myBuffer.begin(); * * // Create the camera for the framebuffer. * myCamera = myBuffer.createCamera(); * * myBuffer.end(); * ``` * * Calling setCamera() updates the * framebuffer's projection using the camera. * resetMatrix() must also be called for the * view to change properly: * * ```js * myBuffer.begin(); * * // Set the camera for the framebuffer. * setCamera(myCamera); * * // Reset all transformations. * resetMatrix(); * * // Draw stuff... * * myBuffer.end(); * ``` * * @returns {p5.Camera} new camera. * * @example * // Double-click to toggle between cameras. * * let myBuffer; * let cam1; * let cam2; * let usingCam1 = true; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * // Create the cameras between begin() and end(). * myBuffer.begin(); * * // Create the first camera. * // Keep its default settings. * cam1 = myBuffer.createCamera(); * * // Create the second camera. * // Place it at the top-left. * // Point it at the origin. * cam2 = myBuffer.createCamera(); * cam2.setPosition(400, -400, 800); * cam2.lookAt(0, 0, 0); * * myBuffer.end(); * * describe( * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.' * ); * } * * function draw() { * // Draw to the p5.Framebuffer object. * myBuffer.begin(); * background(200); * * // Set the camera. * if (usingCam1 === true) { * setCamera(cam1); * } else { * setCamera(cam2); * } * * // Reset all transformations. * resetMatrix(); * * // Draw the box. * box(); * * myBuffer.end(); * * // Display the p5.Framebuffer object. * image(myBuffer, -50, -50); * } * * // Toggle the current camera when the user double-clicks. * function doubleClicked() { * if (usingCam1 === true) { * usingCam1 = false; * } else { * usingCam1 = true; * } * } */ createCamera() { const cam = new FramebufferCamera(this); cam._computeCameraDefaultSettings(); cam._setDefaultCamera(); return cam; } /** * Deletes the framebuffer from GPU memory. * * Calling `myBuffer.remove()` frees the GPU memory used by the framebuffer. * The framebuffer also uses a bit of memory on the CPU which can be freed * like so: * * ```js * // Delete the framebuffer from GPU memory. * myBuffer.remove(); * * // Delete the framebuffer from CPU memory. * myBuffer = undefined; * ``` * * Note: All variables that reference the framebuffer must be assigned * the value `undefined` to delete the framebuffer from CPU memory. If any * variable still refers to the framebuffer, then it won't be garbage * collected. * * @example * // Double-click to remove the p5.Framebuffer object. * * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create an options object. * let options = { width: 60, height: 60 }; * * // Create a p5.Framebuffer object and * // configure it using options. * myBuffer = createFramebuffer(options); * * describe('A white circle at the center of a dark gray square disappears when the user double-clicks.'); * } * * function draw() { * background(200); * * // Display the p5.Framebuffer object if * // it's available. * if (myBuffer) { * // Draw to the p5.Framebuffer object. * myBuffer.begin(); * background(100); * circle(0, 0, 20); * myBuffer.end(); * * image(myBuffer, -30, -30); * } * } * * // Remove the p5.Framebuffer object when the * // the user double-clicks. * function doubleClicked() { * // Delete the framebuffer from GPU memory. * myBuffer.remove(); * * // Delete the framebuffer from CPU memory. * myBuffer = undefined; * } */ remove() { this._deleteTextures(); // Let renderer clean up framebuffer resources this.renderer.deleteFramebufferResources(this); this.renderer.framebuffers.delete(this); } /** * Begins drawing shapes to the framebuffer. * * `myBuffer.begin()` and myBuffer.end() * allow shapes to be drawn to the framebuffer. `myBuffer.begin()` begins * drawing to the framebuffer and * myBuffer.end() stops drawing to the * framebuffer. Changes won't be visible until the framebuffer is displayed * as an image or texture. * * @example * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); * } * * function draw() { * background(200); * * // Start drawing to the p5.Framebuffer object. * myBuffer.begin(); * * background(50); * rotateY(frameCount * 0.01); * normalMaterial(); * torus(30); * * // Stop drawing to the p5.Framebuffer object. * myBuffer.end(); * * // Display the p5.Framebuffer object while * // the user presses the mouse. * if (mouseIsPressed === true) { * image(myBuffer, -50, -50); * } * } */ begin() { this.prevFramebuffer = this.renderer.activeFramebuffer(); if (this.prevFramebuffer) { this.prevFramebuffer._beforeEnd(); } this.renderer.activeFramebuffers.push(this); this._beforeBegin(); this.renderer.push(); // Apply the framebuffer's camera. This does almost what // RendererGL.reset() does, but this does not try to clear any buffers; // it only sets the camera. // this.renderer.setCamera(this.defaultCamera); this.renderer.states.setValue('curCamera', this.defaultCamera); // set the projection matrix (which is not normally updated each frame) this.renderer.states.setValue('uPMatrix', this.renderer.states.uPMatrix.clone()); this.renderer.states.uPMatrix.set(this.defaultCamera.projMatrix); this.renderer.states.setValue('uViewMatrix', this.renderer.states.uViewMatrix.clone()); this.renderer.states.uViewMatrix.set(this.defaultCamera.cameraMatrix); this.renderer.resetMatrix(); this.renderer.states.uViewMatrix .set(this.renderer.states.curCamera.cameraMatrix); this.renderer.states.uModelMatrix.reset(); this.renderer._applyStencilTestIfClipping(); } /** * When making a p5.Framebuffer active so that it may be drawn to, this method * returns the underlying WebGL framebuffer that needs to be active to * support this. Antialiased framebuffers first write to a multisampled * renderbuffer, while other framebuffers can write directly to their main * framebuffers. * * @private */ _framebufferToBind() { return this.renderer.getFramebufferToBind(this); } /** * Ensure all readable textures are up-to-date. * @private * @param {'colorTexutre'|'depthTexture'} property The property to update */ _update(property) { if (this.dirty[property]) { this.renderer.updateFramebufferTexture(this, property); this.dirty[property] = false; } } /** * Ensures that the framebuffer is ready to be drawn to * * @private */ _beforeBegin() { this.renderer.bindFramebuffer(this); this.renderer.viewport( this.width * this.density, this.height * this.density ); if (this.renderer.flushDraw) { this.renderer.flushDraw(); } } /** * Ensures that the framebuffer is ready to be read by other framebuffers. * * @private */ _beforeEnd() { if (this.antialias) { this.dirty = { colorTexture: true, depthTexture: true }; } // TODO // This should work but flushes more often than we need to. Ideally we only do this // right before the fbo is read as a texture. if (this.renderer.flushDraw) { this.renderer.flushDraw(); } } /** * Stops drawing shapes to the framebuffer. * * myBuffer.begin() and `myBuffer.end()` * allow shapes to be drawn to the framebuffer. * myBuffer.begin() begins drawing to * the framebuffer and `myBuffer.end()` stops drawing to the framebuffer. * Changes won't be visible until the framebuffer is displayed as an image * or texture. * * @example * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); * } * * function draw() { * background(200); * * // Start drawing to the p5.Framebuffer object. * myBuffer.begin(); * * background(50); * rotateY(frameCount * 0.01); * normalMaterial(); * torus(30); * * // Stop drawing to the p5.Framebuffer object. * myBuffer.end(); * * // Display the p5.Framebuffer object while * // the user presses the mouse. * if (mouseIsPressed === true) { * image(myBuffer, -50, -50); * } * } */ end() { this.renderer.pop(); const fbo = this.renderer.activeFramebuffers.pop(); if (fbo !== this) { throw new Error("It looks like you've called end() while another Framebuffer is active."); } this._beforeEnd(); if (this.prevFramebuffer) { this.prevFramebuffer._beforeBegin(); } else { this.renderer.bindFramebuffer(null); this.renderer.viewport( this.renderer._origViewport.width, this.renderer._origViewport.height ); } this.renderer._applyStencilTestIfClipping(); } /** * Draws to the framebuffer by calling a function that contains drawing * instructions. * * The parameter, `callback`, is a function with the drawing instructions * for the framebuffer. For example, calling `myBuffer.draw(myFunction)` * will call a function named `myFunction()` to draw to the framebuffer. * Doing so has the same effect as the following: * * ```js * myBuffer.begin(); * myFunction(); * myBuffer.end(); * ``` * * @param {Function} callback function that draws to the framebuffer. * * @example * // Click the canvas to display the framebuffer. * * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); * } * * function draw() { * background(200); * * // Draw to the p5.Framebuffer object. * myBuffer.draw(bagel); * * // Display the p5.Framebuffer object while * // the user presses the mouse. * if (mouseIsPressed === true) { * image(myBuffer, -50, -50); * } * } * * // Draw a rotating, multicolor torus. * function bagel() { * background(50); * rotateY(frameCount * 0.01); * normalMaterial(); * torus(30); * } */ draw(callback) { this.begin(); callback(); this.end(); } /** * Loads the current value of each pixel in the framebuffer into its * pixels array. * * `myBuffer.loadPixels()` must be called before reading from or writing to * myBuffer.pixels. * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Framebuffer object. * let myBuffer = createFramebuffer(); * * // Load the pixels array. * myBuffer.loadPixels(); * * // Get the number of pixels in the * // top half of the framebuffer. * let numPixels = myBuffer.pixels.length / 2; * * // Set the framebuffer's top half to pink. * for (let i = 0; i < numPixels; i += 4) { * myBuffer.pixels[i] = 255; * myBuffer.pixels[i + 1] = 102; * myBuffer.pixels[i + 2] = 204; * myBuffer.pixels[i + 3] = 255; * } * * // Update the pixels array. * myBuffer.updatePixels(); * * // Draw the p5.Framebuffer object to the canvas. * image(myBuffer, -50, -50); * * describe('A pink rectangle above a gray rectangle.'); * } */ loadPixels() { this._update('colorTexture'); const result = this.renderer.readFramebufferPixels(this); // Check if renderer returned a Promise (WebGPU) or data directly (WebGL) if (result && typeof result.then === 'function') { // WebGPU async case - return Promise return result.then(pixels => { this.pixels = pixels; return pixels; }); } else { // WebGL sync case - assign directly this.pixels = result; return result; } } /** * Gets a pixel or a region of pixels from the framebuffer. * * `myBuffer.get()` is easy to use but it's not as fast as * myBuffer.pixels. Use * myBuffer.pixels to read many pixel * values. * * The version of `myBuffer.get()` with no parameters returns the entire * framebuffer as a a p5.Image object. * * The version of `myBuffer.get()` with two parameters interprets them as * coordinates. It returns an array with the `[R, G, B, A]` values of the * pixel at the given point. * * The version of `myBuffer.get()` with four parameters interprets them as * coordinates and dimensions. It returns a subsection of the framebuffer as * a p5.Image object. The first two parameters are * the coordinates for the upper-left corner of the subsection. The last two * parameters are the width and height of the subsection. * * @param {Number} x x-coordinate of the pixel. Defaults to 0. * @param {Number} y y-coordinate of the pixel. Defaults to 0. * @param {Number} w width of the subsection to be returned. * @param {Number} h height of the subsection to be returned. * @return {p5.Image} subsection as a p5.Image object. */ /** * @return {p5.Image} entire framebuffer as a p5.Image object. */ /** * @param {Number} x * @param {Number} y * @return {Number[]} color of the pixel at `(x, y)` as an array of color values `[R, G, B, A]`. */ get(x, y, w, h) { this._update('colorTexture'); // p5._validateParameters('p5.Framebuffer.get', arguments); if (x === undefined && y === undefined) { x = 0; y = 0; w = this.width; h = this.height; } else if (w === undefined && h === undefined) { if (x < 0 || y < 0 || x >= this.width || y >= this.height) { console.warn( 'The x and y values passed to p5.Framebuffer.get are outside of its range and will be clamped.' ); x = constrain(x, 0, this.width - 1); y = constrain(y, 0, this.height - 1); } return this.renderer.readFramebufferPixel(this, x * this.density, y * this.density); } x = constrain(x, 0, this.width - 1); y = constrain(y, 0, this.height - 1); w = constrain(w, 1, this.width - x); h = constrain(h, 1, this.height - y); return this.renderer.readFramebufferRegion(this, x, y, w, h); } /** * Updates the framebuffer with the RGBA values in the * pixels array. * * `myBuffer.updatePixels()` only needs to be called after changing values * in the myBuffer.pixels array. Such * changes can be made directly after calling * myBuffer.loadPixels(). * * @method updatePixels * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Framebuffer object. * let myBuffer = createFramebuffer(); * * // Load the pixels array. * myBuffer.loadPixels(); * * // Get the number of pixels in the * // top half of the framebuffer. * let numPixels = myBuffer.pixels.length / 2; * * // Set the framebuffer's top half to pink. * for (let i = 0; i < numPixels; i += 4) { * myBuffer.pixels[i] = 255; * myBuffer.pixels[i + 1] = 102; * myBuffer.pixels[i + 2] = 204; * myBuffer.pixels[i + 3] = 255; * } * * // Update the pixels array. * myBuffer.updatePixels(); * * // Draw the p5.Framebuffer object to the canvas. * image(myBuffer, -50, -50); * * describe('A pink rectangle above a gray rectangle.'); * } */ updatePixels() { // Let renderer handle the pixel update process this.renderer.updateFramebufferPixels(this); } }; function framebuffer(p5, fn){ /** * A p5.Camera attached to a * p5.Framebuffer. * * @class p5.FramebufferCamera * @param {p5.Framebuffer} framebuffer The framebuffer this camera is * attached to * @private */ p5.FramebufferCamera = FramebufferCamera; /** * A p5.Texture corresponding to a property of a * p5.Framebuffer. * * @class p5.FramebufferTexture * @param {p5.Framebuffer} framebuffer The framebuffer represented by this * texture * @param {String} property The property of the framebuffer represented by * this texture, either `color` or `depth` * @private */ p5.FramebufferTexture = FramebufferTexture; /** * A class to describe a high-performance drawing surface for textures. * * Each `p5.Framebuffer` object provides a dedicated drawing surface called * a *framebuffer*. They're similar to * p5.Graphics objects but can run much faster. * Performance is improved because the framebuffer shares the same WebGL * context as the canvas used to create it. * * `p5.Framebuffer` objects have all the drawing features of the main * canvas. Drawing instructions meant for the framebuffer must be placed * between calls to * myBuffer.begin() and * myBuffer.end(). The resulting image * can be applied as a texture by passing the `p5.Framebuffer` object to the * texture() function, as in `texture(myBuffer)`. * It can also be displayed on the main canvas by passing it to the * image() function, as in `image(myBuffer, 0, 0)`. * * Note: createFramebuffer() is the * recommended way to create an instance of this class. * * @class p5.Framebuffer * @param {p5.Graphics|p5} target sketch instance or * p5.Graphics * object. * @param {Object} [settings] configuration options. */ p5.Framebuffer = Framebuffer$1; /** * An object that stores the framebuffer's color data. * * Each framebuffer uses a * WebGLTexture * object internally to store its color data. The `myBuffer.color` property * makes it possible to pass this data directly to other functions. For * example, calling `texture(myBuffer.color)` or * `myShader.setUniform('colorTexture', myBuffer.color)` may be helpful for * advanced use cases. * * Note: By default, a framebuffer's y-coordinates are flipped compared to * images and videos. It's easy to flip a framebuffer's y-coordinates as * needed when applying it as a texture. For example, calling * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer. * * @property {p5.FramebufferTexture} color * @for p5.Framebuffer * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Framebuffer object. * let myBuffer = createFramebuffer(); * * // Start drawing to the p5.Framebuffer object. * myBuffer.begin(); * * triangle(-25, 25, 0, -25, 25, 25); * * // Stop drawing to the p5.Framebuffer object. * myBuffer.end(); * * // Use the p5.Framebuffer object's WebGLTexture. * texture(myBuffer.color); * * // Style the plane. * noStroke(); * * // Draw the plane. * plane(myBuffer.width, myBuffer.height); * * describe('A white triangle on a gray background.'); * } */ /** * An object that stores the framebuffer's depth data. * * Each framebuffer uses a * WebGLTexture * object internally to store its depth data. The `myBuffer.depth` property * makes it possible to pass this data directly to other functions. For * example, calling `texture(myBuffer.depth)` or * `myShader.setUniform('depthTexture', myBuffer.depth)` may be helpful for * advanced use cases. * * Note: By default, a framebuffer's y-coordinates are flipped compared to * images and videos. It's easy to flip a framebuffer's y-coordinates as * needed when applying it as a texture. For example, calling * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer. * * @property {p5.FramebufferTexture} depth * @for p5.Framebuffer * * @example * // Note: A "uniform" is a global variable within a shader program. * * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; * attribute vec3 aPosition; * attribute vec2 aTexCoord; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * varying vec2 vTexCoord; * * void main() { * vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); * gl_Position = uProjectionMatrix * viewModelPosition; * vTexCoord = aTexCoord; * } * `; * * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` * precision highp float; * varying vec2 vTexCoord; * uniform sampler2D depth; * * void main() { * // Get the pixel's depth value. * float depthVal = texture2D(depth, vTexCoord).r; * * // Set the pixel's color based on its depth. * gl_FragColor = mix( * vec4(0., 0., 0., 1.), * vec4(1., 0., 1., 1.), * depthVal); * } * `; * * let myBuffer; * let myShader; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * // Create a p5.Shader object. * myShader = createShader(vertSrc, fragSrc); * * // Compile and apply the shader. * shader(myShader); * * describe('The shadow of a box rotates slowly against a magenta background.'); * } * * function draw() { * // Draw to the p5.Framebuffer object. * myBuffer.begin(); * background(255); * rotateX(frameCount * 0.01); * box(20, 20, 80); * myBuffer.end(); * * // Set the shader's depth uniform using * // the framebuffer's depth texture. * myShader.setUniform('depth', myBuffer.depth); * * // Style the plane. * noStroke(); * * // Draw the plane. * plane(myBuffer.width, myBuffer.height); * } */ /** * An array containing the color of each pixel in the framebuffer. * * myBuffer.loadPixels() must be * called before accessing the `myBuffer.pixels` array. * myBuffer.updatePixels() * must be called after any changes are made. * * Note: Updating pixels via this property is slower than drawing to the * framebuffer directly. Consider using a * p5.Shader object instead of looping over * `myBuffer.pixels`. * * @property {Number[]} pixels * @for p5.Framebuffer * * @example * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Create a p5.Framebuffer object. * let myBuffer = createFramebuffer(); * * // Load the pixels array. * myBuffer.loadPixels(); * * // Get the number of pixels in the * // top half of the framebuffer. * let numPixels = myBuffer.pixels.length / 2; * * // Set the framebuffer's top half to pink. * for (let i = 0; i < numPixels; i += 4) { * myBuffer.pixels[i] = 255; * myBuffer.pixels[i + 1] = 102; * myBuffer.pixels[i + 2] = 204; * myBuffer.pixels[i + 3] = 255; * } * * // Update the pixels array. * myBuffer.updatePixels(); * * // Draw the p5.Framebuffer object to the canvas. * image(myBuffer, -50, -50); * * describe('A pink rectangle above a gray rectangle.'); * } */ /** * The current width of the framebuffer. * * @property {Number} width * @for p5.Framebuffer */ /** * The current width of the framebuffer. * * @property {Number} height * @for p5.Framebuffer */ } if(typeof p5 !== 'undefined'){ framebuffer(p5); } /** * @module Rendering * @submodule Rendering * @for p5 */ let renderers; function rendering(p5, fn){ // Extend additional renderers object to p5 class, new renderer can be similarly attached if (!p5.renderers) { p5.renderers = {}; } renderers = p5.renderers; /** * Creates a canvas element on the web page. * * `createCanvas()` creates the main drawing canvas for a sketch. It should * only be called once at the beginning of setup(). * Calling `createCanvas()` more than once causes unpredictable behavior. * * The first two parameters, `width` and `height`, are optional. They set the * dimensions of the canvas and the values of the * width and height system * variables. For example, calling `createCanvas(900, 500)` creates a canvas * that's 900×500 pixels. By default, `width` and `height` are both 100. * * The third parameter is also optional. If either of the constants `P2D` or * `WEBGL` is passed, as in `createCanvas(900, 500, WEBGL)`, then it will set * the sketch's rendering mode. If an existing * HTMLCanvasElement * is passed, as in `createCanvas(900, 500, myCanvas)`, then it will be used * by the sketch. To use `WEBGPU` mode, make sure you have the WebGPU mode addon included. * * The fourth parameter is also optional. If an existing * HTMLCanvasElement * is passed, as in `createCanvas(900, 500, WEBGL, myCanvas)`, then it will be * used by the sketch. * * Note: In WebGL mode, the canvas will use a WebGL2 context if it's supported * by the browser. Check the webglVersion * system variable to check what version is being used, or call * `setAttributes({ version: 1 })` to create a WebGL1 context. * * @method createCanvas * @param {Number} [width] width of the canvas. Defaults to 100. * @param {Number} [height] height of the canvas. Defaults to 100. * @param {(P2D|WEBGL|P2DHDR|WEBGPU)} [renderer] either P2D, WEBGL, or WEBGPU. Defaults to `P2D`. * @param {HTMLCanvasElement} [canvas] existing canvas element that should be used for the sketch. * @return {p5.Renderer} new `p5.Renderer` that holds the canvas. * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw a diagonal line. * line(0, 0, width, height); * * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 50); * * background(200); * * // Draw a diagonal line. * line(0, 0, width, height); * * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); * } * * @example * // Use WebGL mode. * * function setup() { * createCanvas(100, 100, WEBGL); * * background(200); * * // Draw a diagonal line. * line(-width / 2, -height / 2, width / 2, height / 2); * * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); * } * * @example * function setup() { * // Create a p5.Render object. * let cnv = createCanvas(50, 50); * * // Position the canvas. * cnv.position(10, 20); * * background(200); * * // Draw a diagonal line. * line(0, 0, width, height); * * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); * } */ /** * @method createCanvas * @param {Number} [width] * @param {Number} [height] * @param {HTMLCanvasElement} [canvas] * @return {p5.Renderer} */ fn.createCanvas = function (w, h, renderer, ...args) { // p5._validateParameters('createCanvas', arguments); //optional: renderer, otherwise defaults to p2d let selectedRenderer = P2D; // Check third argument whether it is renderer constants if(Reflect.ownKeys(renderers).includes(renderer)){ selectedRenderer = renderer; }else { args.unshift(renderer); } // Init our graphics renderer if(this._renderer) this._renderer.remove(); this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args); this._defaultGraphicsCreated = true; this._elements.push(this._renderer); this._renderer._applyDefaults(); // Make the renderer own `pixels` if (!Object.hasOwn(this, 'pixels')) { Object.defineProperty(this, 'pixels', { get(){ return this._renderer?.pixels; } }); } if (this._renderer.contextReady) { return this._renderer.contextReady.then(() => this._renderer); } else { return this._renderer; } }; /** * Resizes the canvas to a given width and height. * * `resizeCanvas()` immediately clears the canvas and calls * redraw(). It's common to call `resizeCanvas()` * within the body of windowResized() like * so: * * ```js * function windowResized() { * resizeCanvas(windowWidth, windowHeight); * } * ``` * * The first two parameters, `width` and `height`, set the dimensions of the * canvas. They also the values of the width and * height system variables. For example, calling * `resizeCanvas(300, 500)` resizes the canvas to 300×500 pixels, then sets * width to 300 and * height 500. * * The third parameter, `noRedraw`, is optional. If `true` is passed, as in * `resizeCanvas(300, 500, true)`, then the canvas will be canvas to 300×500 * pixels but the redraw() function won't be called * immediately. By default, redraw() is called * immediately when `resizeCanvas()` finishes executing. * * @method resizeCanvas * @param {Number} width width of the canvas. * @param {Number} height height of the canvas. * @param {Boolean} [noRedraw] whether to delay calling * redraw(). Defaults * to `false`. * * @example * // Double-click to resize the canvas. * * function setup() { * createCanvas(100, 100); * * describe( * 'A white circle drawn on a gray background. The canvas shrinks by half the first time the user double-clicks.' * ); * } * * function draw() { * background(200); * * // Draw a circle at the center of the canvas. * circle(width / 2, height / 2, 20); * } * * // Resize the canvas when the user double-clicks. * function doubleClicked() { * resizeCanvas(50, 50); * } * * @example * // Resize the web browser to change the canvas size. * * function setup() { * createCanvas(windowWidth, windowHeight); * * describe('A white circle drawn on a gray background.'); * } * * function draw() { * background(200); * * // Draw a circle at the center of the canvas. * circle(width / 2, height / 2, 20); * } * * // Always resize the canvas to fill the browser window. * function windowResized() { * resizeCanvas(windowWidth, windowHeight); * } */ fn.resizeCanvas = function (w, h, noRedraw) { // p5._validateParameters('resizeCanvas', arguments); if (this._renderer) { // Make sure width and height are updated before the renderer resizes so // that framebuffers updated from the resize read the correct size this._renderer.resize(w, h); if (!noRedraw) { this.redraw(); } } //accessible Outputs if (this._addAccsOutput()) { this._updateAccsOutput(); } }; /** * Removes the default canvas. * * By default, a 100×100 pixels canvas is created without needing to call * createCanvas(). `noCanvas()` removes the * default canvas for sketches that don't need it. * * @method noCanvas * * @example * function setup() { * noCanvas(); * } */ fn.noCanvas = function () { if (this.canvas) { this.canvas.parentNode.removeChild(this.canvas); } }; /** * Creates a p5.Graphics object. * * `createGraphics()` creates an offscreen drawing canvas (graphics buffer) * and returns it as a p5.Graphics object. Drawing * to a separate graphics buffer can be helpful for performance and for * organizing code. * * The first two parameters, `width` and `height`, are optional. They set the * dimensions of the p5.Graphics object. For * example, calling `createGraphics(900, 500)` creates a graphics buffer * that's 900×500 pixels. * * The third parameter is also optional. If either of the constants `P2D` or * `WEBGL` is passed, as in `createGraphics(900, 500, WEBGL)`, then it will set * the p5.Graphics object's rendering mode. If an * existing * HTMLCanvasElement * is passed, as in `createGraphics(900, 500, myCanvas)`, then it will be used * by the graphics buffer. * * The fourth parameter is also optional. If an existing * HTMLCanvasElement * is passed, as in `createGraphics(900, 500, WEBGL, myCanvas)`, then it will be * used by the graphics buffer. * * Note: In WebGL mode, the p5.Graphics object * will use a WebGL2 context if it's supported by the browser. Check the * webglVersion system variable to check what * version is being used, or call `setAttributes({ version: 1 })` to create a * WebGL1 context. * * @method createGraphics * @param {Number} width width of the graphics buffer. * @param {Number} height height of the graphics buffer. * @param {(P2D|WEBGL)} [renderer] either P2D or WEBGL. Defaults to P2D. * @param {HTMLCanvasElement} [canvas] existing canvas element that should be * used for the graphics buffer.. * @return {p5.Graphics} new graphics buffer. * * @example * // Double-click to draw the contents of the graphics buffer. * * let pg; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the p5.Graphics object. * pg = createGraphics(50, 50); * * // Draw to the graphics buffer. * pg.background(100); * pg.circle(pg.width / 2, pg.height / 2, 20); * * describe('A gray square. A smaller, darker square with a white circle at its center appears when the user double-clicks.'); * } * * // Display the graphics buffer when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * image(pg, 25, 25); * } * } * * @example * // Double-click to draw the contents of the graphics buffer. * * let pg; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create the p5.Graphics object in WebGL mode. * pg = createGraphics(50, 50, WEBGL); * * // Draw to the graphics buffer. * pg.background(100); * pg.lights(); * pg.noStroke(); * pg.rotateX(QUARTER_PI); * pg.rotateY(QUARTER_PI); * pg.torus(15, 5); * * describe('A gray square. A smaller, darker square with a white torus at its center appears when the user double-clicks.'); * } * * // Display the graphics buffer when the user double-clicks. * function doubleClicked() { * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { * image(pg, 25, 25); * } * } */ /** * @method createGraphics * @param {Number} width * @param {Number} height * @param {HTMLCanvasElement} [canvas] * @return {p5.Graphics} */ fn.createGraphics = function (w, h, ...args) { /** * args[0] is expected to be renderer * args[1] is expected to be canvas */ if (args[0] instanceof HTMLCanvasElement) { args[1] = args[0]; args[0] = P2D; } // p5._validateParameters('createGraphics', arguments); return new p5.Graphics(w, h, args[0], this, args[1]); }; /** * Creates and a new p5.Framebuffer object. * * p5.Framebuffer objects are separate drawing * surfaces that can be used as textures in WebGL mode. They're similar to * p5.Graphics objects and generally run much * faster when used as textures. * * The parameter, `options`, is optional. An object can be passed to configure * the p5.Framebuffer object. The available * properties are: * * - `format`: data format of the texture, either `UNSIGNED_BYTE`, `FLOAT`, or `HALF_FLOAT`. Default is `UNSIGNED_BYTE`. * - `channels`: whether to store `RGB` or `RGBA` color channels. Default is to match the main canvas which is `RGBA`. * - `depth`: whether to include a depth buffer. Default is `true`. * - `depthFormat`: data format of depth information, either `UNSIGNED_INT` or `FLOAT`. Default is `FLOAT`. * - `stencil`: whether to include a stencil buffer for masking. `depth` must be `true` for this feature to work. Defaults to the value of `depth` which is `true`. * - `antialias`: whether to perform anti-aliasing. If set to `true`, as in `{ antialias: true }`, 2 samples will be used by default. The number of samples can also be set, as in `{ antialias: 4 }`. Default is to match setAttributes() which is `false` (`true` in Safari). * - `width`: width of the p5.Framebuffer object. Default is to always match the main canvas width. * - `height`: height of the p5.Framebuffer object. Default is to always match the main canvas height. * - `density`: pixel density of the p5.Framebuffer object. Default is to always match the main canvas pixel density. * - `textureFiltering`: how to read values from the p5.Framebuffer object. Either `LINEAR` (nearby pixels will be interpolated) or `NEAREST` (no interpolation). Generally, use `LINEAR` when using the texture as an image and `NEAREST` if reading the texture as data. Default is `LINEAR`. * * If the `width`, `height`, or `density` attributes are set, they won't automatically match the main canvas and must be changed manually. * * Note: `createFramebuffer()` can only be used in WebGL mode. * * @method createFramebuffer * @param {Object} [options] configuration options. * @param {UNSIGNED_BYTE|FLOAT|HALF_FLOAT} [options.format=UNSIGNED_BYTE] The data format of the texture. * @param {RGB|RGBA} [options.channels=RGBA] What color channels to include in the texture. * @param {Boolean} [options.depth=true] Whether to store depth information in the framebuffer. * @param {UNSIGNED_INT|FLOAT} [options.depthFormat=FLOAT] The format to store depth values in. * @param {Boolean} [options.stencil=true] Whether to include a stencil buffer (required for clipping.) * @param {Boolean|Number} [options.antialias] Whether to antialias when drawing to this framebuffer. Either a boolean, or the number of antialias samples to use. * @param {Number} [options.width] The width of the framebuffer. By default, it will match the main canvas. * @param {Number} [options.height] The height of the framebuffer. By default, it will match the main canvas. * @param {Number} [options.density] The pixel density of the framebuffer. By default, it will match the main canvas. * @param {LINEAR|NEAREST} [options.textureFiltering=LINEAR] The strategy used when reading values in the framebuffer in between pixels. * @return {p5.Framebuffer} new framebuffer. * * @example * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create a p5.Framebuffer object. * myBuffer = createFramebuffer(); * * describe('A grid of white toruses rotating against a dark gray background.'); * } * * function draw() { * background(50); * * // Start drawing to the p5.Framebuffer object. * myBuffer.begin(); * * // Clear the drawing surface. * clear(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * * // Style the torus. * noStroke(); * * // Draw the torus. * torus(20); * * // Stop drawing to the p5.Framebuffer object. * myBuffer.end(); * * // Iterate from left to right. * for (let x = -50; x < 50; x += 25) { * // Iterate from top to bottom. * for (let y = -50; y < 50; y += 25) { * // Draw the p5.Framebuffer object to the canvas. * image(myBuffer, x, y, 25, 25); * } * } * } * * @example * let myBuffer; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create an options object. * let options = { width: 25, height: 25 }; * * // Create a p5.Framebuffer object. * // Use options for configuration. * myBuffer = createFramebuffer(options); * * describe('A grid of white toruses rotating against a dark gray background.'); * } * * function draw() { * background(50); * * // Start drawing to the p5.Framebuffer object. * myBuffer.begin(); * * // Clear the drawing surface. * clear(); * * // Turn on the lights. * lights(); * * // Rotate the coordinate system. * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * * // Style the torus. * noStroke(); * * // Draw the torus. * torus(5, 2.5); * * // Stop drawing to the p5.Framebuffer object. * myBuffer.end(); * * // Iterate from left to right. * for (let x = -50; x < 50; x += 25) { * // Iterate from top to bottom. * for (let y = -50; y < 50; y += 25) { * // Draw the p5.Framebuffer object to the canvas. * image(myBuffer, x, y); * } * } * } */ fn.createFramebuffer = function (options) { return new Framebuffer$1(this._renderer, options); }; /** * Clears the depth buffer in WebGL mode. * * `clearDepth()` clears information about how far objects are from the camera * in 3D space. This information is stored in an object called the * *depth buffer*. Clearing the depth buffer ensures new objects aren't drawn * behind old ones. Doing so can be useful for feedback effects in which the * previous frame serves as the background for the current frame. * * The parameter, `depth`, is optional. If a number is passed, as in * `clearDepth(0.5)`, it determines the range of objects to clear from the * depth buffer. 0 doesn't clear any depth information, 0.5 clears depth * information halfway between the near and far clipping planes, and 1 clears * depth information all the way to the far clipping plane. By default, * `depth` is 1. * * Note: `clearDepth()` can only be used in WebGL mode. * * @method clearDepth * @param {Number} [depth] amount of the depth buffer to clear between 0 * (none) and 1 (far clipping plane). Defaults to 1. * * @example * let previous; * let current; * * function setup() { * createCanvas(100, 100, WEBGL); * * // Create the p5.Framebuffer objects. * previous = createFramebuffer({ format: FLOAT }); * current = createFramebuffer({ format: FLOAT }); * * describe( * 'A multicolor box drifts from side to side on a white background. It leaves a trail that fades over time.' * ); * } * * function draw() { * // Swap the previous p5.Framebuffer and the * // current one so it can be used as a texture. * [previous, current] = [current, previous]; * * // Start drawing to the current p5.Framebuffer. * current.begin(); * * // Paint the background. * background(255); * * // Draw the previous p5.Framebuffer. * // Clear the depth buffer so the previous * // frame doesn't block the current one. * push(); * tint(255, 250); * image(previous, -50, -50); * clearDepth(); * pop(); * * // Draw the box on top of the previous frame. * push(); * let x = 25 * sin(frameCount * 0.01); * let y = 25 * sin(frameCount * 0.02); * translate(x, y, 0); * rotateX(frameCount * 0.01); * rotateY(frameCount * 0.01); * normalMaterial(); * box(12); * pop(); * * // Stop drawing to the current p5.Framebuffer. * current.end(); * * // Display the current p5.Framebuffer. * image(current, -50, -50); * } */ fn.clearDepth = function (depth) { this._assert3d('clearDepth'); this._renderer.clearDepth(depth); }; /** * A system variable that provides direct access to the sketch's * `<canvas>` element. * * The `<canvas>` element provides many specialized features that aren't * included in the p5.js library. The `drawingContext` system variable * provides access to these features by exposing the sketch's * CanvasRenderingContext2D * object. * * @property {CanvasRenderingContext2D|WebGLRenderingContext|WebGL2RenderingContext} drawingContext * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the circle using shadows. * drawingContext.shadowOffsetX = 5; * drawingContext.shadowOffsetY = -5; * drawingContext.shadowBlur = 10; * drawingContext.shadowColor = 'black'; * * // Draw the circle. * circle(50, 50, 40); * * describe("A white circle on a gray background. The circle's edges are shadowy."); * } * * @example * function setup() { * createCanvas(100, 100); * * background('skyblue'); * * // Style the circle using a color gradient. * let myGradient = drawingContext.createRadialGradient(50, 50, 3, 50, 50, 40); * myGradient.addColorStop(0, 'yellow'); * myGradient.addColorStop(0.6, 'orangered'); * myGradient.addColorStop(1, 'yellow'); * drawingContext.fillStyle = myGradient; * drawingContext.strokeStyle = 'rgba(0, 0, 0, 0)'; * * // Draw the circle. * circle(50, 50, 40); * * describe('A fiery sun drawn on a light blue background.'); * } */ } if(typeof p5 !== 'undefined'){ rendering(p5, p5.prototype); } var filterBaseFrag = "precision highp float;\n\nuniform sampler2D tex0;\nuniform vec2 canvasSize;\nuniform vec2 texelSize;\n\nIN vec2 vTexCoord;\n\nstruct FilterInputs {\n vec2 texCoord;\n vec2 canvasSize;\n vec2 texelSize;\n};\n\nvoid main(void) {\n FilterInputs inputs;\n inputs.texCoord = vTexCoord;\n inputs.canvasSize = canvasSize;\n inputs.texelSize = texelSize;\n OUT_COLOR = HOOK_getColor(inputs, tex0);\n OUT_COLOR.rgb *= outColor.a;\n}\n"; var filterBaseVert = "precision highp int;\n\nuniform mat4 uModelViewMatrix;\nuniform mat4 uProjectionMatrix;\n\nIN vec3 aPosition;\nIN vec2 aTexCoord;\nOUT vec2 vTexCoord;\n\nvoid main() {\n // transferring texcoords for the frag shader\n vTexCoord = aTexCoord;\n\n // copy position with a fourth coordinate for projection (1.0 is normal)\n vec4 positionVec4 = vec4(aPosition, 1.0);\n\n // project to 3D space\n gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;\n}\n"; var webgl2CompatibilityShader = "#ifdef WEBGL2\n\n#define IN in\n#define OUT out\n\n#ifdef FRAGMENT_SHADER\nout vec4 outColor;\n#define OUT_COLOR outColor\n#endif\n#define TEXTURE texture\n\n#else\n\n#ifdef FRAGMENT_SHADER\n#define IN varying\n#else\n#define IN attribute\n#endif\n#define OUT varying\n#define TEXTURE texture2D\n\n#ifdef FRAGMENT_SHADER\n#define OUT_COLOR gl_FragColor\n#endif\n\n#endif\n\nvec4 getTexture(in sampler2D content, vec2 coord) {\n vec4 color = TEXTURE(content, coord);\n if (color.a > 0.) color.rgb /= color.a;\n return color;\n}\n"; ///////////////////// // Enums for nodes // ///////////////////// const NodeType = { OPERATION: 'operation', LITERAL: 'literal', VARIABLE: 'variable', CONSTANT: 'constant', STRUCT: 'struct', PHI: 'phi', STATEMENT: 'statement', ASSIGNMENT: 'assignment', }; const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); const NodeTypeRequiredFields = { [NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"], [NodeType.LITERAL]: ["value", "dimension", "baseType"], [NodeType.VARIABLE]: ["identifier", "dimension", "baseType"], [NodeType.CONSTANT]: ["value", "dimension", "baseType"], [NodeType.STRUCT]: [""], [NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"], [NodeType.STATEMENT]: ["statementType"], [NodeType.ASSIGNMENT]: ["dependsOn"] }; const StatementType = { DISCARD: 'discard', BREAK: 'break', EARLY_RETURN: 'early_return', EXPRESSION: 'expression', // Used when we want to output a single expression as a statement, e.g. a for loop condition EMPTY: 'empty', // Used for empty statements like ; in for loops }; const BaseType = { FLOAT: "float", INT: "int", BOOL: "bool", MAT: "mat", DEFER: "defer", SAMPLER2D: "sampler2D", SAMPLER: "sampler", }; const BasePriority = { [BaseType.FLOAT]: 3, [BaseType.INT]: 2, [BaseType.BOOL]: 1, [BaseType.MAT]: 0, [BaseType.DEFER]: -1, [BaseType.SAMPLER2D]: -10, [BaseType.SAMPLER]: -11, }; const DataType = { float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, }, float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3, }, float4: { fnName: "vec4", baseType: BaseType.FLOAT, dimension:4, priority: 3, }, int1: { fnName: "int", baseType: BaseType.INT, dimension:1, priority: 2, }, int2: { fnName: "ivec2", baseType: BaseType.INT, dimension:2, priority: 2, }, int3: { fnName: "ivec3", baseType: BaseType.INT, dimension:3, priority: 2, }, int4: { fnName: "ivec4", baseType: BaseType.INT, dimension:4, priority: 2, }, bool1: { fnName: "bool", baseType: BaseType.BOOL, dimension:1, priority: 1, }, bool2: { fnName: "bvec2", baseType: BaseType.BOOL, dimension:2, priority: 1, }, bool3: { fnName: "bvec3", baseType: BaseType.BOOL, dimension:3, priority: 1, }, bool4: { fnName: "bvec4", baseType: BaseType.BOOL, dimension:4, priority: 1, }, mat2: { fnName: "mat2x2", baseType: BaseType.MAT, dimension:2, priority: 0, }, mat3: { fnName: "mat3x3", baseType: BaseType.MAT, dimension:3, priority: 0, }, mat4: { fnName: "mat4x4", baseType: BaseType.MAT, dimension:4, priority: 0, }, defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, sampler2D: { fnName: "sampler2D", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 }, sampler: { fnName: "sampler", baseType: BaseType.SAMPLER, dimension: 1, priority: -11 }, }; const structType = function (hookType) { let T = hookType.type === undefined ? hookType : hookType.type; const structType = { name: hookType.name, properties: [], typeName: T.typeName, }; // TODO: handle struct properties that are themselves structs for (const prop of T.properties) { const propType = prop.type.dataType; structType.properties.push( {name: prop.name, dataType: propType } ); } return structType; }; function isStructType(typeInfo) { return !!(typeInfo && typeInfo.properties); } const GenType = { FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 }, INT: { baseType: BaseType.INT, dimension: null, priority: 2 }, BOOL: { baseType: BaseType.BOOL, dimension: null, priority: 1 }, }; function typeEquals(nodeA, nodeB) { return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType); } const TypeInfoFromGLSLName = Object.fromEntries( Object.values(DataType) .filter(info => info.fnName !== null) .map(info => [info.fnName, info]) ); const OpCode = { Binary: { ADD: 0, SUBTRACT: 1, MULTIPLY: 2, DIVIDE: 3, MODULO: 4, EQUAL: 5, NOT_EQUAL: 6, GREATER_THAN: 7, GREATER_EQUAL: 8, LESS_THAN: 9, LESS_EQUAL: 10, LOGICAL_AND: 11, LOGICAL_OR: 12, MEMBER_ACCESS: 13, }, Unary: { LOGICAL_NOT: 100, NEGATE: 101, PLUS: 102, SWIZZLE: 103, }, Nary: { FUNCTION_CALL: 200, CONSTRUCTOR: 201, }}; const OperatorTable = [ { arity: "unary", name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT }, { arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE }, { arity: "unary", name: "plus", symbol: "+", opCode: OpCode.Unary.PLUS }, { arity: "binary", name: "add", symbol: "+", opCode: OpCode.Binary.ADD }, { arity: "binary", name: "sub", symbol: "-", opCode: OpCode.Binary.SUBTRACT }, { arity: "binary", name: "mult", symbol: "*", opCode: OpCode.Binary.MULTIPLY }, { arity: "binary", name: "div", symbol: "/", opCode: OpCode.Binary.DIVIDE }, { arity: "binary", name: "mod", symbol: "%", opCode: OpCode.Binary.MODULO }, { arity: "binary", name: "equalTo", symbol: "==", opCode: OpCode.Binary.EQUAL }, { arity: "binary", name: "notEqual", symbol: "!=", opCode: OpCode.Binary.NOT_EQUAL }, { arity: "binary", name: "greaterThan", symbol: ">", opCode: OpCode.Binary.GREATER_THAN }, { arity: "binary", name: "greaterEqual", symbol: ">=", opCode: OpCode.Binary.GREATER_EQUAL }, { arity: "binary", name: "lessThan", symbol: "<", opCode: OpCode.Binary.LESS_THAN }, { arity: "binary", name: "lessEqual", symbol: "<=", opCode: OpCode.Binary.LESS_EQUAL }, { arity: "binary", name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND }, { arity: "binary", name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR }, ]; // export const SymbolToOpCode = {}; const OpCodeToSymbol = {}; const UnarySymbolToName = {}; for (const { symbol, opCode, name, arity } of OperatorTable) { // SymbolToOpCode[symbol] = opCode; OpCodeToSymbol[opCode] = symbol; if (arity === 'unary') { UnarySymbolToName[symbol] = name; } } const BlockType = { GLOBAL: 'global', FUNCTION: 'function', BRANCH: 'branch', IF_COND: 'if_cond', IF_BODY: 'if_body', ELSE_COND: 'else_cond', SCOPE_START: 'scope_start', SCOPE_END: 'scope_end', FOR: 'for', MERGE: 'merge', DEFAULT: 'default', }; Object.fromEntries( Object.entries(BlockType).map(([key, val]) => [val, key]) ); function internalError(errorMessage) { const prefixedMessage = `[p5.strands internal error]: ${errorMessage}`; throw new Error(prefixedMessage); } function userError(errorType, errorMessage) { const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`; throw new Error(prefixedMessage); } ///////////////////////////////// // Public functions for strands runtime ///////////////////////////////// function createDirectedAcyclicGraph() { const graph = { nextID: 0, cache: new Map(), nodeTypes: [], baseTypes: [], dimensions: [], opCodes: [], values: [], identifiers: [], phiBlocks: [], dependsOn: [], usedBy: [], statementTypes: [], swizzles: [], }; return graph; } function getOrCreateNode(graph, node) { // const key = getNodeKey(node); // const existing = graph.cache.get(key); // if (existing !== undefined) { // return existing; // } else { const id = createNode(graph, node); // graph.cache.set(key, id); return id; // } } function createNodeData(data = {}) { const node = { nodeType: data.nodeType ?? null, baseType: data.baseType ?? null, dimension: data.dimension ?? null, opCode: data.opCode ?? null, value: data.value ?? null, identifier: data.identifier ?? null, statementType: data.statementType ?? null, swizzle: data.swizzle ?? null, dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [], usedBy: Array.isArray(data.usedBy) ? data.usedBy : [], phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [], }; validateNode(node); return node; } function getNodeDataFromID(graph, id) { return { id, nodeType: graph.nodeTypes[id], opCode: graph.opCodes[id], value: graph.values[id], identifier: graph.identifiers[id], dependsOn: graph.dependsOn[id], usedBy: graph.usedBy[id], phiBlocks: graph.phiBlocks[id], dimension: graph.dimensions[id], baseType: graph.baseTypes[id], statementType: graph.statementTypes[id], swizzle: graph.swizzles[id], } } function extractNodeTypeInfo(dag, nodeID) { return { baseType: dag.baseTypes[nodeID], dimension: dag.dimensions[nodeID], priority: BasePriority[dag.baseTypes[nodeID]], }; } ///////////////////////////////// // Private functions ///////////////////////////////// function createNode(graph, node) { const id = graph.nextID++; graph.nodeTypes[id] = node.nodeType; graph.opCodes[id] = node.opCode; graph.values[id] = node.value; graph.identifiers[id] = node.identifier; graph.dependsOn[id] = node.dependsOn.slice(); graph.usedBy[id] = node.usedBy; graph.phiBlocks[id] = node.phiBlocks.slice(); graph.baseTypes[id] = node.baseType; graph.dimensions[id] = node.dimension; graph.statementTypes[id] = node.statementType; graph.swizzles[id] = node.swizzle; for (const dep of node.dependsOn) { if (!Array.isArray(graph.usedBy[dep])) { graph.usedBy[dep] = []; } graph.usedBy[dep].push(id); } return id; } function validateNode(node){ const nodeType = node.nodeType; const requiredFields = NodeTypeRequiredFields[nodeType]; if (requiredFields.length === 2) { internalError(`Required fields for node type '${NodeTypeToName[nodeType]}' not defined. Please add them to the utils.js file in p5.strands!`); } const missingFields = []; for (const field of requiredFields) { if (node[field] === null) { missingFields.push(field); } } if (node.dependsOn?.some(v => v === undefined)) { throw new Error('Undefined dependency!'); } if (missingFields.length > 0) { internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } } // Todo: remove edges to simplify. Block order is always ordered already. function createControlFlowGraph() { return { // graph structure blockTypes: [], incomingEdges: [], outgoingEdges: [], blockInstructions: [], // runtime data for constructing graph nextID: 0, blockStack: [], blockOrder: [], blockConditions: {}, currentBlock: -1, }; } function pushBlock(graph, blockID) { graph.blockStack.push(blockID); graph.blockOrder.push(blockID); graph.currentBlock = blockID; } function popBlock(graph) { graph.blockStack.pop(); const len = graph.blockStack.length; graph.currentBlock = graph.blockStack[len-1]; } function pushBlockForModification(graph, blockID) { graph.blockStack.push(blockID); graph.currentBlock = blockID; } function createBasicBlock(graph, blockType) { const id = graph.nextID++; graph.blockTypes[id] = blockType; graph.incomingEdges[id] = []; graph.outgoingEdges[id] = []; graph.blockInstructions[id]= []; return id; } function addEdge(graph, from, to) { graph.outgoingEdges[from].push(to); graph.incomingEdges[to].push(from); } function recordInBasicBlock(graph, blockID, nodeID) { if (nodeID === undefined) { internalError('undefined nodeID in `recordInBasicBlock()`'); } if (blockID === undefined) { internalError('undefined blockID in `recordInBasicBlock()'); } graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; graph.blockInstructions[blockID].push(nodeID); } function sortCFG(adjacencyList, start) { const visited = new Set(); const postOrder = []; function dfs(v) { if (visited.has(v)) { return; } visited.add(v); for (let w of adjacencyList[v].sort((a, b) => b-a) || []) { dfs(w); } postOrder.push(v); } dfs(start); return postOrder.reverse(); } class StrandsNode { constructor(id, dimension, strandsContext) { this.id = id; this.strandsContext = strandsContext; this.dimension = dimension; this.structProperties = null; this.isStrandsNode = true; // Store original identifier for varying variables const dag = this.strandsContext.dag; const nodeData = getNodeDataFromID(dag, this.id); if (nodeData && nodeData.identifier) { this._originalIdentifier = nodeData.identifier; } if (nodeData) { this._originalBaseType = nodeData.baseType; this._originalDimension = nodeData.dimension; } } withStructProperties(properties) { this.structProperties = properties; return this; } copy() { return createStrandsNode(this.id, this.dimension, this.strandsContext); } typeInfo() { return { baseType: this._originalBaseType || BaseType.FLOAT, dimension: this.dimension }; } bridge(value) { const { dag, cfg } = this.strandsContext; const orig = getNodeDataFromID(dag, this.id); const baseType = orig?.baseType ?? BaseType.FLOAT; let newValueID; if (value instanceof StrandsNode) { newValueID = value.id; } else { const newVal = primitiveConstructorNode( this.strandsContext, { baseType, dimension: this.dimension }, value ); newValueID = newVal.id; } // For varying variables, we need both assignment generation AND a way to reference by identifier if (this._originalIdentifier) { // Create a variable node for the target (the varying variable) const { id: targetVarID } = variableNode( this.strandsContext, { baseType: this._originalBaseType, dimension: this._originalDimension }, this._originalIdentifier ); // Create assignment node for GLSL generation const assignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [targetVarID, newValueID], phiBlocks: [] }); const assignmentID = getOrCreateNode(dag, assignmentNode); recordInBasicBlock(cfg, cfg.currentBlock, assignmentID); // Track for global assignments processing this.strandsContext.globalAssignments.push(assignmentID); // Simply update this node to be a variable node with the identifier // This ensures it always generates the variable name in expressions const variableNodeData = createNodeData({ nodeType: NodeType.VARIABLE, baseType: this._originalBaseType, dimension: this._originalDimension, identifier: this._originalIdentifier }); const variableID = getOrCreateNode(dag, variableNodeData); this.id = variableID; // Point to the variable node for expression generation } else { this.id = newValueID; // For non-varying variables, just update to new value } return this; } bridgeSwizzle(swizzlePattern, value) { const { dag, cfg } = this.strandsContext; const orig = getNodeDataFromID(dag, this.id); const baseType = orig?.baseType ?? BaseType.FLOAT; let newValueID; if (value instanceof StrandsNode) { newValueID = value.id; } else { const newVal = primitiveConstructorNode( this.strandsContext, { baseType, dimension: this.dimension }, value ); newValueID = newVal.id; } // For varying variables, create swizzle assignment if (this._originalIdentifier) { // Create a variable node for the target with swizzle const { id: targetVarID } = variableNode( this.strandsContext, { baseType: this._originalBaseType, dimension: this._originalDimension }, this._originalIdentifier ); // Create a swizzle node for the target (myVarying.xyz) const swizzleNode = createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Unary.SWIZZLE, baseType: this._originalBaseType, dimension: swizzlePattern.length, // xyz = 3, xy = 2, etc. swizzle: swizzlePattern, dependsOn: [targetVarID] }); const swizzleID = getOrCreateNode(dag, swizzleNode); // Create assignment node: myVarying.xyz = value const assignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [swizzleID, newValueID], phiBlocks: [] }); const assignmentID = getOrCreateNode(dag, assignmentNode); recordInBasicBlock(cfg, cfg.currentBlock, assignmentID); // Track for global assignments processing in the current hook context this.strandsContext.globalAssignments.push(assignmentID); // Simply update this node to be a variable node with the identifier // This ensures it always generates the variable name in expressions const variableNodeData = createNodeData({ nodeType: NodeType.VARIABLE, baseType: this._originalBaseType, dimension: this._originalDimension, identifier: this._originalIdentifier }); const variableID = getOrCreateNode(dag, variableNodeData); this.id = variableID; // Point to the variable node, not the assignment node } else { this.id = newValueID; // For non-varying variables, just update to new value } return this; } getValue() { if (this._originalIdentifier) { const { id, dimension } = variableNode( this.strandsContext, { baseType: this._originalBaseType, dimension: this._originalDimension }, this._originalIdentifier ); return createStrandsNode(id, dimension, this.strandsContext); } return this; } } function createStrandsNode(id, dimension, strandsContext, onRebind) { return new Proxy( new StrandsNode(id, dimension, strandsContext), swizzleTrap(id, dimension, strandsContext, onRebind) ); } // Need the .js extension because we also import this from a Node script. // Try to keep this file minimal because of that. // GLSL Built in functions // https://docs.gl/el3/abs const builtInGLSLFunctions = { //////////// Trigonometry ////////// acos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], acosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], asin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], asinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], atan: [ { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, ], atanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], cos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], cosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], degrees: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], radians: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], sin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT , isp5Function: true}], sinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], tan: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], tanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], ////////// Mathematics ////////// abs: [ { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, { params: [GenType.FLOAT], returnType: GenType.INT, isp5Function: true} ], ceil: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], clamp: [ { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, { params: [GenType.FLOAT,DataType.float1,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, { params: [GenType.INT, GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: false}, { params: [GenType.INT, DataType.int1, DataType.int1], returnType: GenType.INT, isp5Function: false}, ], dFdx: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], dFdy: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], exp: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], exp2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], floor: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], fma: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], fract: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], fwidth: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], inversesqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], // "isinf": [{}], // "isnan": [{}], log: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], log2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], max: [ { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, ], min: [ { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true}, { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true}, ], mix: [ { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, { params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}, { params: [GenType.FLOAT, GenType.FLOAT, GenType.BOOL], returnType: GenType.FLOAT, isp5Function: false}, ], mod: [ { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}, { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true}, ], // "modf": [{}], pow: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], round: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], roundEven: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], sign: [ { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, { params: [GenType.INT], returnType: GenType.INT, isp5Function: false}, ], smoothstep: [ { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, { params: [ DataType.float1,DataType.float1, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}, ], sqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], step: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], trunc: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], ////////// Vector ////////// cross: [{ params: [DataType.float3, DataType.float3], returnType: DataType.float3, isp5Function: true}], distance: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], dot: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}], equal: [ { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, ], faceforward: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], length: [{ params: [GenType.FLOAT], returnType:DataType.float1, isp5Function: false}], normalize: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}], notEqual: [ { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false}, { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false}, { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false}, ], reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}], }; const strandsBuiltinFunctions = { ...builtInGLSLFunctions, }; ////////////////////////////////////////////// // Builders for node graphs ////////////////////////////////////////////// function scalarLiteralNode(strandsContext, typeInfo, value) { const { cfg, dag } = strandsContext; let { dimension, baseType } = typeInfo; if (dimension !== 1) { internalError('Created a scalar literal node with dimension > 1.'); } const nodeData = createNodeData({ nodeType: NodeType.LITERAL, dimension, baseType, value }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension }; } function variableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; const { dimension, baseType } = typeInfo; const nodeData = createNodeData({ nodeType: NodeType.VARIABLE, dimension, baseType, identifier }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension }; } function unaryOpNode(strandsContext, nodeOrValue, opCode) { const { dag, cfg } = strandsContext; let dependsOn; let node; if (nodeOrValue instanceof StrandsNode) { node = nodeOrValue; } else { const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, nodeOrValue); node = createStrandsNode(id, dimension, strandsContext); } dependsOn = [node.id]; const nodeData = createNodeData({ nodeType: NodeType.OPERATION, opCode, dependsOn, baseType: dag.baseTypes[node.id], dimension: node.dimension }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: node.dimension }; } function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. let rightStrandsNode; if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) { rightStrandsNode = rightArg[0]; } else { const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, rightArg); rightStrandsNode = createStrandsNode(id, dimension, strandsContext); } let finalLeftNodeID = leftStrandsNode.id; let finalRightNodeID = rightStrandsNode.id; // Check if we have to cast either node const leftType = extractNodeTypeInfo(dag, leftStrandsNode.id); const rightType = extractNodeTypeInfo(dag, rightStrandsNode.id); const cast = { node: null, toType: leftType }; const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER; if (bothDeferred) { cast.toType.baseType = BaseType.FLOAT; if (leftType.dimension === rightType.dimension) { cast.toType.dimension = leftType.dimension; } else if (leftType.dimension === 1 && rightType.dimension > 1) { cast.toType.dimension = rightType.dimension; } else if (rightType.dimension === 1 && leftType.dimension > 1) { cast.toType.dimension = leftType.dimension; } else { userError("type error", `You have tried to perform a binary operation:\n`+ `${leftType.baseType+leftType.dimension} ${OpCodeToSymbol[opCode]} ${rightType.baseType+rightType.dimension}\n` + `It's only possible to operate on two nodes with the same dimension, or a scalar value and a vector.` ); } const l = primitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode); const r = primitiveConstructorNode(strandsContext, cast.toType, rightStrandsNode); finalLeftNodeID = l.id; finalRightNodeID = r.id; } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { if (leftType.dimension === 1 && rightType.dimension > 1) { cast.node = leftStrandsNode; cast.toType = rightType; } else if (rightType.dimension === 1 && leftType.dimension > 1) { cast.node = rightStrandsNode; cast.toType = leftType; } else if (leftType.priority > rightType.priority) { // e.g. op(float vector, int vector): cast priority is float > int > bool cast.node = rightStrandsNode; cast.toType = leftType; } else if (rightType.priority > leftType.priority) { cast.node = leftStrandsNode; cast.toType = rightType; } else { userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } const casted = primitiveConstructorNode(strandsContext, cast.toType, cast.node); if (cast.node === leftStrandsNode) { leftStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); finalLeftNodeID = leftStrandsNode.id; } else { rightStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); finalRightNodeID = rightStrandsNode.id; } } const nodeData = createNodeData({ nodeType: NodeType.OPERATION, opCode, dependsOn: [finalLeftNodeID, finalRightNodeID], baseType: cast.toType.baseType, dimension: cast.toType.dimension, }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: nodeData.dimension }; } function structInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { const { cfg, dag } = strandsContext; if (dependsOn.length === 0) { for (const prop of structTypeInfo.properties) { const typeInfo = prop.dataType; const nodeData = createNodeData({ nodeType: NodeType.VARIABLE, baseType: typeInfo.baseType, dimension: typeInfo.dimension, identifier: `${identifier}.${prop.name}`, }); const componentID = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, componentID); dependsOn.push(componentID); } } const nodeData = createNodeData({ nodeType: NodeType.VARIABLE, dimension: structTypeInfo.properties.length, baseType: structTypeInfo.typeName, identifier, dependsOn }); const structID = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, structID); return { id: structID, dimension: 0, components: dependsOn }; } function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { const inputs = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; const dag = strandsContext.dag; let calculatedDimensions = 0; let originalNodeID = null; for (const dep of inputs.flat(Infinity)) { if (dep && dep.isStrandsNode) { const node = getNodeDataFromID(dag, dep.id); originalNodeID = dep.id; baseType = node.baseType; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { for (const inner of node.dependsOn) { mappedDependencies.push(inner); } } else { mappedDependencies.push(dep.id); } calculatedDimensions += node.dimension; continue; } else if (typeof dep === 'number') { const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(id); calculatedDimensions += dimension; continue; } else { userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`); } } if (dimension === null) { dimension = calculatedDimensions; } else if (dimension > calculatedDimensions && calculatedDimensions === 1) { calculatedDimensions = dimension; } else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) { userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`); } const inferredTypeInfo = { dimension, baseType, priority: BasePriority[baseType], }; return { originalNodeID, mappedDependencies, inferredTypeInfo }; } function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) { const nodeData = createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, dimension: typeInfo.dimension, baseType: typeInfo.baseType, dependsOn: strandsNodesArray }); const id = getOrCreateNode(strandsContext.dag, nodeData); return id; } function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) { const cfg = strandsContext.cfg; const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn); const finalType = { baseType: typeInfo.baseType, dimension: inferredTypeInfo.dimension }; const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); if (typeInfo.baseType !== BaseType.DEFER) { recordInBasicBlock(cfg, cfg.currentBlock, id); } return { id, dimension: finalType.dimension, components: mappedDependencies }; } function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { const { cfg, dag } = strandsContext; const { properties } = structTypeInfo; if (!(rawUserArgs.length === properties.length)) { userError('type error', `You've tried to construct a ${structTypeInfo.typeName} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + `The properties it expects are:\n` + `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}` ); } const dependsOn = []; for (let i = 0; i < properties.length; i++) { const expectedProperty = properties[i]; const { originalNodeID, mappedDependencies } = mapPrimitiveDepsToIDs(strandsContext, expectedProperty.dataType, rawUserArgs[i]); if (originalNodeID) { dependsOn.push(originalNodeID); } else { dependsOn.push( constructTypeFromIDs(strandsContext, expectedProperty.dataType, mappedDependencies) ); } } const nodeData = createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, dimension: properties.length, baseType: structTypeInfo.typeName , dependsOn }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: properties.length, components: structTypeInfo.components }; } function functionCallNode( strandsContext, functionName, rawUserArgs, { overloads: rawOverloads } = {}, ) { const { cfg, dag } = strandsContext; const overloads = rawOverloads || strandsBuiltinFunctions[functionName]; const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg)); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { const argsLengthSet = new Set(); const argsLengthArr = []; overloads.forEach((overload) => argsLengthSet.add(overload.params.length)); argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`)); const argsLengthStr = argsLengthArr.join(', or '); userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); } const isGeneric = (T) => T.dimension === null; let bestOverload = null; let bestScore = 0; let inferredReturnType = null; let inferredDimension = null; for (const overload of matchingArgsCounts) { let isValid = true; let similarity = 0; for (let i = 0; i < preprocessedArgs.length; i++) { const preArg = preprocessedArgs[i]; const argType = preArg.inferredTypeInfo; const expectedType = overload.params[i]; let dimension = expectedType.dimension; if (isGeneric(expectedType)) { if (inferredDimension === null || inferredDimension === 1) { inferredDimension = argType.dimension; } if (inferredDimension !== argType.dimension && !(argType.dimension === 1 && inferredDimension >= 1) ) { isValid = false; } dimension = inferredDimension; } else { if (argType.dimension > dimension) { isValid = false; } } if (argType.baseType === expectedType.baseType) { similarity += 2; } else if(expectedType.priority > argType.priority) { similarity += 1; } } if (isValid && (!bestOverload || similarity > bestScore)) { bestOverload = overload; bestScore = similarity; inferredReturnType = {...overload.returnType }; if (isGeneric(inferredReturnType)) { inferredReturnType.dimension = inferredDimension; } } } if (bestOverload === null) { userError('parameter validation', `No matching overload for ${functionName} was found!`); } let dependsOn = []; for (let i = 0; i < bestOverload.params.length; i++) { const arg = preprocessedArgs[i]; const paramType = { ...bestOverload.params[i] }; if (isGeneric(paramType)) { paramType.dimension = inferredDimension; } if (arg.originalNodeID && typeEquals(arg.inferredTypeInfo, paramType)) { dependsOn.push(arg.originalNodeID); } else { const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies); recordInBasicBlock(cfg, cfg.currentBlock, castedArgID); dependsOn.push(castedArgID); } } const nodeData = createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, identifier: functionName, dependsOn, baseType: inferredReturnType.baseType, dimension: inferredReturnType.dimension }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: inferredReturnType.dimension }; } function statementNode(strandsContext, statementType) { const { dag, cfg } = strandsContext; const nodeData = createNodeData({ nodeType: NodeType.STATEMENT, statementType }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return id; } function swizzleNode(strandsContext, parentNode, swizzle) { const { dag, cfg } = strandsContext; const baseType = dag.baseTypes[parentNode.id]; const nodeData = createNodeData({ nodeType: NodeType.OPERATION, baseType, dimension: swizzle.length, opCode: OpCode.Unary.SWIZZLE, dependsOn: [parentNode.id], swizzle, }); const id = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: swizzle.length }; } function swizzleTrap(id, dimension, strandsContext, onRebind) { const swizzleSets = [ ['x', 'y', 'z', 'w'], ['r', 'g', 'b', 'a'], ['s', 't', 'p', 'q'] ].map(s => s.slice(0, dimension)); const trap = { get(target, property, receiver) { if (property in target) { return Reflect.get(...arguments); } else { for (const set of swizzleSets) { if ([...property.toString()].every(char => set.includes(char))) { const swizzle = [...property].map(char => { const index = set.indexOf(char); return swizzleSets[0][index]; }).join(''); const node = swizzleNode(strandsContext, target, swizzle); return createStrandsNode(node.id, node.dimension, strandsContext); } } } }, set(target, property, value, receiver) { for (const swizzleSet of swizzleSets) { const chars = [...property]; const valid = chars.every(c => swizzleSet.includes(c)) && new Set(chars).size === chars.length && target.dimension >= chars.length; if (!valid) continue; const dim = target.dimension; // lanes are the underlying values of the target vector // e.g. lane 0 holds the value aliased by 'x', 'r', and 's' // the lanes array is in the 'correct' order const lanes = new Array(dim); for (let i = 0; i < dim; i++) { const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]); lanes[i] = createStrandsNode(id, dimension, strandsContext); } // The scalars array contains the individual components of the users values. // This may not be the most efficient way, as we swizzle each component individually, // so that .xyz becomes .x, .y, .z let scalars = []; if (value instanceof StrandsNode) { if (value.dimension === 1) { scalars = Array(chars.length).fill(value); } else if (value.dimension === chars.length) { for (let k = 0; k < chars.length; k++) { const { id, dimension } = swizzleNode(strandsContext, value, 'xyzw'[k]); scalars.push(createStrandsNode(id, dimension, strandsContext)); } } else { userError('type error', `Swizzle assignment: RHS vector does not match LHS vector (need ${chars.length}, got ${value.dimension}).`); } } else if (Array.isArray(value)) { const flat = value.flat(Infinity); if (flat.length === 1) { scalars = Array(chars.length).fill(flat[0]); } else if (flat.length === chars.length) { scalars = flat; } else { userError('type error', `Swizzle assignment: RHS length ${flat.length} does not match ${chars.length}.`); } } else if (typeof value === 'number') { scalars = Array(chars.length).fill(value); } else { userError('type error', `Unsupported RHS for swizzle assignment: ${value}`); } // The canonical index refers to the actual value's position in the vector lanes // i.e. we are finding (3,2,1) from .zyx // We set the correct value in the lanes array for (let j = 0; j < chars.length; j++) { const canonicalIndex = swizzleSet.indexOf(chars[j]); lanes[canonicalIndex] = scalars[j]; } const orig = getNodeDataFromID(strandsContext.dag, target.id); const baseType = orig?.baseType ?? BaseType.FLOAT; const { id: newID } = primitiveConstructorNode( strandsContext, { baseType, dimension: dim }, lanes ); target.id = newID; // If we swizzle assign on a struct component i.e. // inputs.position.rg = [1, 2] // The onRebind callback will update the structs components so that it refers to the new values, // and make a new ID for the struct with these new values if (typeof onRebind === 'function') { onRebind(newID); } return true; } return Reflect.set(...arguments); } }; return trap; } function shouldCreateTemp(dag, nodeID) { const nodeType = dag.nodeTypes[nodeID]; if (nodeType !== NodeType.OPERATION) return false; if (dag.baseTypes[nodeID] === BaseType.SAMPLER2D) return false; const uses = dag.usedBy[nodeID] || []; return uses.length > 1; } const TypeNames = { 'float1': 'float', 'float2': 'vec2', 'float3': 'vec3', 'float4': 'vec4', 'int1': 'int', 'int2': 'ivec2', 'int3': 'ivec3', 'int4': 'ivec4', 'bool1': 'bool', 'bool2': 'bvec2', 'bool3': 'bvec3', 'bool4': 'bvec4', 'mat2': 'mat2x2', 'mat3': 'mat3x3', 'mat4': 'mat4x4', }; const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { const { dag, cfg } = strandsContext; const instructions = cfg.blockInstructions[blockID] || []; for (const nodeID of instructions) { const nodeType = dag.nodeTypes[nodeID]; if (shouldCreateTemp(dag, nodeID)) { const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); generationContext.write(declaration); } if (nodeType === NodeType.STATEMENT) { glslBackend.generateStatement(generationContext, dag, nodeID); } if (nodeType === NodeType.ASSIGNMENT) { glslBackend.generateAssignment(generationContext, dag, nodeID); generationContext.visitedNodes.add(nodeID); } } }, [BlockType.BRANCH](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; // Find all phi nodes in this branch block and declare them const blockInstructions = cfg.blockInstructions[blockID] || []; for (const nodeID of blockInstructions) { const node = getNodeDataFromID(dag, nodeID); if (node.nodeType === NodeType.PHI) { // Check if the phi node's first dependency already has a temp name const dependsOn = node.dependsOn || []; if (dependsOn.length > 0) { const firstDependency = dependsOn[0]; const existingTempName = generationContext.tempNames[firstDependency]; if (existingTempName) { // Reuse the existing temp name instead of creating a new one generationContext.tempNames[nodeID] = existingTempName; continue; // Skip declaration, just alias to existing variable } } // Otherwise, create a new temp variable for the phi node const tmp = `T${generationContext.nextTempID++}`; generationContext.tempNames[nodeID] = tmp; const T = extractNodeTypeInfo(dag, nodeID); const typeName = glslBackend.getTypeName(T.baseType, T.dimension); generationContext.write(`${typeName} ${tmp};`); } } this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, [BlockType.IF_COND](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; const conditionID = cfg.blockConditions[blockID]; const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID); generationContext.write(`if (${condExpr})`); this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, [BlockType.ELSE_COND](blockID, strandsContext, generationContext) { generationContext.write(`else`); this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, [BlockType.IF_BODY](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); this.assignPhiNodeValues(blockID, strandsContext, generationContext); }, [BlockType.SCOPE_START](blockID, strandsContext, generationContext) { generationContext.write(`{`); generationContext.indent++; }, [BlockType.SCOPE_END](blockID, strandsContext, generationContext) { generationContext.indent--; generationContext.write(`}`); }, [BlockType.MERGE](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, [BlockType.FUNCTION](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, [BlockType.FOR](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; const instructions = cfg.blockInstructions[blockID] || []; generationContext.write(`for (`); // Set flag to suppress semicolon on the last statement const originalSuppressSemicolon = generationContext.suppressSemicolon; for (let i = 0; i < instructions.length; i++) { const nodeID = instructions[i]; const node = getNodeDataFromID(dag, nodeID); const isLast = i === instructions.length - 1; // Suppress semicolon on the last statement generationContext.suppressSemicolon = isLast; if (shouldCreateTemp(dag, nodeID)) { const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); generationContext.write(declaration); } if (node.nodeType === NodeType.STATEMENT) { glslBackend.generateStatement(generationContext, dag, nodeID); } if (node.nodeType === NodeType.ASSIGNMENT) { glslBackend.generateAssignment(generationContext, dag, nodeID); generationContext.visitedNodes.add(nodeID); } } // Restore original flag generationContext.suppressSemicolon = originalSuppressSemicolon; generationContext.write(`)`); }, assignPhiNodeValues(blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; // Find all phi nodes that this block feeds into const successors = cfg.outgoingEdges[blockID] || []; for (const successorBlockID of successors) { const instructions = cfg.blockInstructions[successorBlockID] || []; for (const nodeID of instructions) { const node = getNodeDataFromID(dag, nodeID); if (node.nodeType === NodeType.PHI) { // Find which input of this phi node corresponds to our block const branchIndex = node.phiBlocks?.indexOf(blockID); if (branchIndex !== -1 && branchIndex < node.dependsOn.length) { const sourceNodeID = node.dependsOn[branchIndex]; const tempName = generationContext.tempNames[nodeID]; if (tempName && sourceNodeID !== null) { const sourceExpr = glslBackend.generateExpression(generationContext, dag, sourceNodeID); generationContext.write(`${tempName} = ${sourceExpr};`); } } } } } }, }; const glslBackend = { hookEntry(hookType) { const firstLine = `(${hookType.parameters.flatMap((param) => { return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`; }).join(', ')}) {`; return firstLine; }, getTypeName(baseType, dimension) { const primitiveTypeName = TypeNames[baseType + dimension]; if (!primitiveTypeName) { return baseType; } return primitiveTypeName; }, generateHookUniformKey(name, typeInfo) { return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; }, generateVaryingVariable(varName, typeInfo) { return `${typeInfo.fnName} ${varName}`; }, generateLocalDeclaration(varName, typeInfo) { const typeName = typeInfo.fnName; return `${typeName} ${varName};`; }, generateStatement(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); // Generate the expression followed by semicolon (unless suppressed) const semicolon = generationContext.suppressSemicolon ? '' : ';'; if (node.statementType === StatementType.DISCARD) { generationContext.write(`discard${semicolon}`); } else if (node.statementType === StatementType.BREAK) { generationContext.write(`break${semicolon}`); } else if (node.statementType === StatementType.EXPRESSION) { const exprNodeID = node.dependsOn[0]; const expr = this.generateExpression(generationContext, dag, exprNodeID); generationContext.write(`${expr}${semicolon}`); } else if (node.statementType === StatementType.EMPTY) { generationContext.write(semicolon); } else if (node.statementType === StatementType.EARLY_RETURN) { const exprNodeID = node.dependsOn[0]; const expr = this.generateExpression(generationContext, dag, exprNodeID); generationContext.write(`return ${expr}${semicolon}`); } }, generateAssignment(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); // dependsOn[0] = targetNodeID, dependsOn[1] = sourceNodeID const targetNodeID = node.dependsOn[0]; const sourceNodeID = node.dependsOn[1]; // Generate the target expression (could be variable or swizzle) const targetExpr = this.generateExpression(generationContext, dag, targetNodeID); const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); const semicolon = generationContext.suppressSemicolon ? '' : ';'; // Generate assignment if we have both target and source if (targetExpr && sourceExpr && targetExpr !== sourceExpr) { generationContext.write(`${targetExpr} = ${sourceExpr}${semicolon}`); } }, generateDeclaration(generationContext, dag, nodeID) { const expr = this.generateExpression(generationContext, dag, nodeID); const tmp = `T${generationContext.nextTempID++}`; generationContext.tempNames[nodeID] = tmp; const T = extractNodeTypeInfo(dag, nodeID); const typeName = this.getTypeName(T.baseType, T.dimension); return `${typeName} ${tmp} = ${expr};`; }, generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { const dag = strandsContext.dag; const rootNode = getNodeDataFromID(dag, rootNodeID); if (isStructType(returnType)) { const structTypeInfo = returnType; for (let i = 0; i < structTypeInfo.properties.length; i++) { const prop = structTypeInfo.properties[i]; const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]); if (prop.name !== val) { generationContext.write( `${rootNode.identifier}.${prop.name} = ${val};` ); } } } generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`); }, generateExpression(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); if (generationContext.tempNames?.[nodeID]) { return generationContext.tempNames[nodeID]; } switch (node.nodeType) { case NodeType.LITERAL: if (node.baseType === BaseType.FLOAT) { return node.value.toFixed(4); } else { return node.value; } case NodeType.VARIABLE: // Track shared variable usage context if (generationContext.shaderContext && generationContext.strandsContext?.sharedVariables?.has(node.identifier)) { const sharedVar = generationContext.strandsContext.sharedVariables.get(node.identifier); if (generationContext.shaderContext === 'vertex') { sharedVar.usedInVertex = true; } else if (generationContext.shaderContext === 'fragment') { sharedVar.usedInFragment = true; } } return node.identifier; case NodeType.OPERATION: const useParantheses = node.usedBy.length > 0; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { // TODO: differentiate casts and constructors for more efficient codegen. // if (node.dependsOn.length === 1 && node.dimension === 1) { // return this.generateExpression(generationContext, dag, node.dependsOn[0]); // } if (node.baseType === BaseType.SAMPLER2D) { return this.generateExpression(generationContext, dag, node.dependsOn[0]); } const T = this.getTypeName(node.baseType, node.dimension); const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); return `${T}(${deps.join(', ')})`; } if (node.opCode === OpCode.Nary.FUNCTION_CALL) { const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); return `${node.identifier}(${functionArgs.join(', ')})`; } if (node.opCode === OpCode.Binary.MEMBER_ACCESS) { const [lID, rID] = node.dependsOn; const lName = this.generateExpression(generationContext, dag, lID); const rName = this.generateExpression(generationContext, dag, rID); return `${lName}.${rName}`; } if (node.opCode === OpCode.Unary.SWIZZLE) { const parentID = node.dependsOn[0]; const parentExpr = this.generateExpression(generationContext, dag, parentID); return `${parentExpr}.${node.swizzle}`; } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); const right = this.generateExpression(generationContext, dag, rID); // Special case for modulo: use mod() function for floats in GLSL if (node.opCode === OpCode.Binary.MODULO) { const leftNode = getNodeDataFromID(dag, lID); const rightNode = getNodeDataFromID(dag, rID); // If either operand is float, use mod() function if (leftNode.baseType === BaseType.FLOAT || rightNode.baseType === BaseType.FLOAT) { return `mod(${left}, ${right})`; } // For integers, use % operator return `(${left} % ${right})`; } const opSym = OpCodeToSymbol[node.opCode]; if (useParantheses) { return `(${left} ${opSym} ${right})`; } else { return `${left} ${opSym} ${right}`; } } if (node.opCode === OpCode.Unary.LOGICAL_NOT || node.opCode === OpCode.Unary.NEGATE || node.opCode === OpCode.Unary.PLUS ) { const [i] = node.dependsOn; const val = this.generateExpression(generationContext, dag, i); const sym = OpCodeToSymbol[node.opCode]; return `${sym}${val}`; } case NodeType.PHI: // Phi nodes represent conditional merging of values // If this phi node has an identifier (like varying variables), use that if (node.identifier) { return node.identifier; } // Otherwise, they should have been declared as temporary variables // and assigned in the appropriate branches if (generationContext.tempNames?.[nodeID]) { return generationContext.tempNames[nodeID]; } else { // If no temp was created, this phi node only has one input // so we can just use that directly const validInputs = node.dependsOn.filter(id => id !== null); if (validInputs.length > 0) { return this.generateExpression(generationContext, dag, validInputs[0]); } else { throw new Error(`No valid inputs for node`) } } case NodeType.ASSIGNMENT: internalError(`ASSIGNMENT nodes should not be used as expressions`); default: internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`); } }, generateBlock(blockID, strandsContext, generationContext) { const type = strandsContext.cfg.blockTypes[blockID]; const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; handler.call(cfgHandlers, blockID, strandsContext, generationContext); }, createGetTextureCall(strandsContext, args) { // In GLSL, getTexture is straightforward - just pass through the args // First argument should be a texture (sampler2D), second should be coordinates const { id, dimension } = functionCallNode(strandsContext, 'getTexture', args, { overloads: [{ params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4 }] }); return { id, dimension }; }, instanceIdReference() { return 'gl_InstanceID'; }, }; /* * Shared utility function for parsing shader hook types from GLSL shader source */ function getShaderHookTypes(shader, hookName) { let fullSrc = shader._vertSrc; let body = shader.hooks.vertex[hookName]; if (!body) { body = shader.hooks.fragment[hookName]; fullSrc = shader._fragSrc; } if (!body) { throw new Error(`Can't find hook ${hookName}!`); } const nameParts = hookName.split(/\s+/g); const functionName = nameParts.pop(); const returnType = nameParts.pop(); const returnQualifiers = [...nameParts]; const parameterMatch = /\(([^\)]*)\)/.exec(body); if (!parameterMatch) { throw new Error(`Couldn't find function parameters in hook body:\n${body}`); } const structProperties = structName => { const structDefMatch = new RegExp(`struct\\s+${structName}\\s*{([^}]*)}`).exec(fullSrc); if (!structDefMatch) return undefined; const properties = []; for (const defSrc of structDefMatch[1].split(';')) { // E.g. `int var1, var2;` or `MyStruct prop;` const parts = defSrc.trim().split(/\s+|,/g); const typeName = parts.shift(); const names = [...parts]; const typeProperties = structProperties(typeName); for (const name of names) { const dataType = TypeInfoFromGLSLName[typeName] || null; properties.push({ name, type: { typeName, qualifiers: [], properties: typeProperties, dataType, } }); } } return properties; }; const parameters = parameterMatch[1].split(',').map(paramString => { // e.g. `int prop` or `in sampler2D prop` or `const float prop` const parts = paramString.trim().split(/\s+/g); const name = parts.pop(); const typeName = parts.pop(); const qualifiers = [...parts]; const properties = structProperties(typeName); const dataType = TypeInfoFromGLSLName[typeName] || null; return { name, type: { typeName, qualifiers, properties, dataType, } }; }); const dataType = TypeInfoFromGLSLName[returnType] || null; return { name: functionName, returnType: { typeName: returnType, qualifiers: returnQualifiers, properties: structProperties(returnType), dataType, }, parameters }; } var noiseGLSL = "// Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl\n// MIT licensed, adapted for p5.strands\n\nvec3 mod289(vec3 x) {\n return x - floor(x * (1.0 / 289.0)) * 289.0;\n}\n\nvec4 mod289(vec4 x) {\n return x - floor(x * (1.0 / 289.0)) * 289.0;\n}\n\nvec4 permute(vec4 x) {\n return mod289(((x*34.0)+10.0)*x);\n}\n\nvec4 taylorInvSqrt(vec4 r)\n{\n return 1.79284291400159 - 0.85373472095314 * r;\n}\n\nfloat baseNoise(vec3 v)\n{\n const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n\n // First corner\n vec3 i = floor(v + dot(v, C.yyy) );\n vec3 x0 = v - i + dot(i, C.xxx) ;\n\n // Other corners\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min( g.xyz, l.zxy );\n vec3 i2 = max( g.xyz, l.zxy );\n\n // x0 = x0 - 0.0 + 0.0 * C.xxx;\n // x1 = x0 - i1 + 1.0 * C.xxx;\n // x2 = x0 - i2 + 2.0 * C.xxx;\n // x3 = x0 - 1.0 + 3.0 * C.xxx;\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y\n vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y\n\n // Permutations\n i = mod289(i);\n vec4 p = permute( permute( permute(\n i.z + vec4(0.0, i1.z, i2.z, 1.0 ))\n + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))\n + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));\n\n // Gradients: 7x7 points over a square, mapped onto an octahedron.\n // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)\n float n_ = 0.142857142857; // 1.0/7.0\n vec3 ns = n_ * D.wyz - D.xzx;\n\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)\n\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)\n\n vec4 x = x_ *ns.x + ns.yyyy;\n vec4 y = y_ *ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n\n vec4 b0 = vec4( x.xy, y.xy );\n vec4 b1 = vec4( x.zw, y.zw );\n\n //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;\n //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;\n vec4 s0 = floor(b0)*2.0 + 1.0;\n vec4 s1 = floor(b1)*2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n\n vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;\n vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;\n\n vec3 p0 = vec3(a0.xy,h.x);\n vec3 p1 = vec3(a0.zw,h.y);\n vec3 p2 = vec3(a1.xy,h.z);\n vec3 p3 = vec3(a1.zw,h.w);\n\n //Normalise gradients\n vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));\n p0 *= norm.x;\n p1 *= norm.y;\n p2 *= norm.z;\n p3 *= norm.w;\n\n // Mix final noise value\n vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n m = m * m;\n return 105.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),\n dot(p2,x2), dot(p3,x3) ) );\n}\n\nfloat noise(vec3 st, int octaves, float ampFalloff) {\n float result = 0.0;\n float amplitude = 1.0;\n float frequency = 1.0;\n\n for (int i = 0; i < 8; i++) {\n if (i >= octaves) break;\n result += amplitude * baseNoise(st * frequency);\n frequency *= 2.0;\n amplitude *= ampFalloff;\n }\n return (result + 1.0) * 0.5;\n}\n"; class FilterRenderer2D { /** * Creates a new FilterRenderer2D instance. * @param {p5} parentRenderer - The p5.js instance. */ constructor(parentRenderer) { this.parentRenderer = parentRenderer; // Create a canvas for applying WebGL-based filters this.canvas = document.createElement('canvas'); this.canvas.width = parentRenderer.width; this.canvas.height = parentRenderer.height; // Initialize the WebGL context let webglVersion = WEBGL2; this.gl = this.canvas.getContext('webgl2'); if (!this.gl) { webglVersion = WEBGL; this.gl = this.canvas.getContext('webgl'); } if (!this.gl) { console.error('WebGL not supported, cannot apply filter.'); return; } this.textures = new Map(); // Minimal renderer object required by p5.Shader and p5.Texture this._renderer = { GL: this.gl, registerEnabled: new Set(), _curShader: null, _emptyTexture: null, webglVersion, states: { textureWrapX: CLAMP, textureWrapY: CLAMP, }, _arraysEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b), _getEmptyTexture: () => { if (!this._emptyTexture) { const im = new Image(1, 1); im.set(0, 0, 255); this._emptyTexture = new Texture(this._renderer, im); } return this._emptyTexture; }, _initShader: (shader) => { const gl = this.gl; const vertShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertShader, shader.vertSrc()); gl.compileShader(vertShader); if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { throw new Error(`Yikes! An error occurred compiling the vertex shader: ${ gl.getShaderInfoLog(vertShader) }`); } const fragShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragShader, shader.fragSrc()); gl.compileShader(fragShader); if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { throw new Error(`Darn! An error occurred compiling the fragment shader: ${ gl.getShaderInfoLog(fragShader) }`); } const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error( `Snap! Error linking shader program: ${gl.getProgramInfoLog(program)}` ); } shader._glProgram = program; shader._vertShader = vertShader; shader._fragShader = fragShader; }, getTexture: (input) => { let src = input; if (src instanceof Framebuffer) { src = src.color; } const texture = this.textures.get(src); if (texture) { return texture; } const tex = new Texture(this._renderer, src); this.textures.set(src, tex); return tex; }, populateHooks: (shader, src, shaderType) => { return populateGLSLHooks(shader, src, shaderType); }, _getShaderAttributes: (shader) => { return getWebGLShaderAttributes(shader, this.gl); }, getUniformMetadata: (shader) => { return getWebGLUniformMetadata(shader, this.gl); }, _finalizeShader: () => {}, _useShader: (shader) => { this.gl.useProgram(shader._glProgram); }, bindTexture: (tex) => { // bind texture using gl context + glTarget and // generated gl texture object this.gl.bindTexture(this.gl.TEXTURE_2D, tex.getTexture().texture); }, unbindTexture: () => { // unbind per above, disable texturing on glTarget this.gl.bindTexture(this.gl.TEXTURE_2D, null); }, _unbindFramebufferTexture: (uniform) => { // Make sure an empty texture is bound to the slot so that we don't // accidentally leave a framebuffer bound, causing a feedback loop // when something else tries to write to it const gl = this.gl; const empty = this._getEmptyTexture(); gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); empty.bindTexture(); gl.uniform1i(uniform.location, uniform.samplerIndex); }, createTexture: ({ width, height, format, dataType }) => { const gl = this.gl; const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // TODO use format and data type return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE }; }, uploadTextureFromSource: ({ texture, glFormat, glDataType }, source) => { const gl = this.gl; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source); }, uploadTextureFromData: ({ texture, glFormat, glDataType }, data, width, height) => { const gl = this.gl; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D( gl.TEXTURE_2D, 0, glFormat, width, height, 0, glFormat, glDataType, data ); }, setTextureParams: (texture) => { return setWebGLTextureParams(texture, this.gl, this._renderer.webglVersion); }, updateUniformValue: (shader, uniform, data) => { return setWebGLUniformValue( shader, uniform, data, (tex) => this._renderer.getTexture(tex), this.gl ); }, _enableAttrib: (_shader, attr, size, type, normalized, stride, offset) => { const loc = attr.location; const gl = this.gl; // Enable register even if it is disabled if (!this._renderer.registerEnabled.has(loc)) { gl.enableVertexAttribArray(loc); // Record register availability this._renderer.registerEnabled.add(loc); } gl.vertexAttribPointer( loc, size, type || gl.FLOAT, normalized || false, stride || 0, offset || 0 ); }, _disableRemainingAttributes: (shader) => { for (const location of this._renderer.registerEnabled.values()) { if ( !Object.keys(shader.attributes).some( key => shader.attributes[key].location === location ) ) { this.gl.disableVertexAttribArray(location); this._renderer.registerEnabled.delete(location); } } }, _updateTexture: (uniform, tex) => { const gl = this.gl; gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); tex.bindTexture(); tex.update(); gl.uniform1i(uniform.location, uniform.samplerIndex); }, baseFilterShader: () => this.baseFilterShader(), strandsBackend: glslBackend, getShaderHookTypes: (shader, hookName) => getShaderHookTypes(shader, hookName), uniformNameFromHookKey: (key) => key.slice(key.indexOf(' ') + 1), }; this._baseFilterShader = undefined; // Store initialized shaders for each operation this.filterShaders = {}; // These will be set by setOperation this.operation = null; this.filterParameter = 1; this.customShader = null; this._shader = null; // Create buffers once this.vertexBuffer = this.gl.createBuffer(); this.texcoordBuffer = this.gl.createBuffer(); // Set up the vertices and texture coordinates for a full-screen quad this.vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); this.texcoords = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]); // Upload vertex data once this._bindBufferData( this.vertexBuffer, this.gl.ARRAY_BUFFER, this.vertices ); // Upload texcoord data once this._bindBufferData( this.texcoordBuffer, this.gl.ARRAY_BUFFER, this.texcoords ); } _webGL2CompatibilityPrefix(shaderType, floatPrecision) { let code = ''; if (this._renderer.webglVersion === WEBGL2) { code += '#version 300 es\n#define WEBGL2\n'; } if (shaderType === 'vert') { code += '#define VERTEX_SHADER\n'; } else if (shaderType === 'frag') { code += '#define FRAGMENT_SHADER\n'; } if (floatPrecision) { code += `precision ${floatPrecision} float;\n`; } return code; } baseFilterShader() { if (!this._baseFilterShader) { this._baseFilterShader = new Shader( this._renderer, this._webGL2CompatibilityPrefix('vert', 'highp') + webgl2CompatibilityShader + filterBaseVert, this._webGL2CompatibilityPrefix('frag', 'highp') + webgl2CompatibilityShader + filterBaseFrag, { vertex: {}, fragment: { 'vec4 getColor': `(FilterInputs inputs, in sampler2D canvasContent) { return getTexture(canvasContent, inputs.texCoord); }` } } ); } return this._baseFilterShader; } getNoiseShaderSnippet() { return noiseGLSL; } /** * Set the current filter operation and parameter. If a customShader is provided, * that overrides the operation-based shader. * @param {String} operation - The filter operation type (e.g., constants.BLUR). * @param {Number} filterParameter - The strength of the filter. * @param {p5.Shader} customShader - Optional custom shader. */ setOperation(operation, filterParameter, customShader = null) { this.operation = operation; this.filterParameter = filterParameter; let useDefaultParam = operation in filterParamDefaults && filterParameter === undefined; if (useDefaultParam) { this.filterParameter = filterParamDefaults[operation]; } this.customShader = customShader; this._initializeShader(); } /** * Initializes or retrieves the shader program for the current operation. * If a customShader is provided, that is used. * Otherwise, returns a cached shader if available, or creates a new one, caches it, and sets it as current. */ _initializeShader() { if (this.customShader) { this._shader = this.customShader; return; } if (!this.operation) { console.error('No operation set for FilterRenderer2D, cannot initialize shader.'); return; } // If we already have a compiled shader for this operation, reuse it if (this.filterShaders[this.operation]) { this._shader = this.filterShaders[this.operation]; return; } // Use the shared makeFilterShader function from filterShaders.js const newShader = makeFilterShader(this._renderer, this.operation, this.parentRenderer._pInst); this.filterShaders[this.operation] = newShader; this._shader = newShader; } /** * Binds a buffer to the drawing context * when passed more than two arguments it also updates or initializes * the data associated with the buffer */ _bindBufferData(buffer, target, values) { const gl = this.gl; gl.bindBuffer(target, buffer); gl.bufferData(target, values, gl.STATIC_DRAW); } get canvasTexture() { if (!this._canvasTexture) { this._canvasTexture = new Texture(this._renderer, this.parentRenderer.wrappedElt); } return this._canvasTexture; } /** * Prepares and runs the full-screen quad draw call. */ _renderPass() { const gl = this.gl; this._shader.bindShader('fill'); const pixelDensity = this.parentRenderer.pixelDensity ? this.parentRenderer.pixelDensity() : 1; const texelSize = [ 1 / (this.parentRenderer.width * pixelDensity), 1 / (this.parentRenderer.height * pixelDensity) ]; const canvasTexture = this.canvasTexture; // Set uniforms for the shader this._shader.setUniform('tex0', canvasTexture); this._shader.setUniform('texelSize', texelSize); this._shader.setUniform('canvasSize', [this.parentRenderer.width, this.parentRenderer.height]); this._shader.setUniform('radius', Math.max(1, this.filterParameter)); this._shader.setUniform('filterParameter', this.filterParameter); this._shader.setDefaultUniforms(); this.parentRenderer.states.setValue('rectMode', CORNER); this.parentRenderer.states.setValue('imageMode', CORNER); this.parentRenderer.blendMode(BLEND); this.parentRenderer.resetMatrix(); const identityMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; this._shader.setUniform('uModelViewMatrix', identityMatrix); this._shader.setUniform('uProjectionMatrix', identityMatrix); // Bind and enable vertex attributes gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); this._shader.enableAttrib(this._shader.attributes.aPosition, 2); gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer); this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2); this._shader.bindTextures(); this._renderer._disableRemainingAttributes(this._shader); // Draw the quad gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // Unbind the shader this._shader.unbindShader(); } /** * Applies the current filter operation. If the filter requires multiple passes (e.g. blur), * it handles those internally. Make sure setOperation() has been called before applyFilter(). */ applyFilter() { if (!this._shader) { console.error('Cannot apply filter: shader not initialized.'); return; } this.parentRenderer.push(); this.parentRenderer.resetMatrix(); // For blur, we typically do two passes: one horizontal, one vertical. if (this.operation === BLUR && !this.customShader) { // Horizontal pass this._shader.setUniform('direction', [1, 0]); this._renderPass(); // Draw the result onto itself this.parentRenderer.clear(); this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, this.parentRenderer.width, this.parentRenderer.height ); // Vertical pass this._shader.setUniform('direction', [0, 1]); this._renderPass(); this.parentRenderer.clear(); this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, this.parentRenderer.width, this.parentRenderer.height ); } else { // Single-pass filters this._renderPass(); this.parentRenderer.clear(); // con this.parentRenderer.blendMode(BLEND); this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, this.parentRenderer.width, this.parentRenderer.height ); } this.parentRenderer.pop(); } } const styleEmpty = 'rgba(0,0,0,0)'; // const alphaThreshold = 0.00125; // minimum visible class Renderer2D extends Renderer { constructor(pInst, w, h, isMainCanvas, elt, attributes = {}) { super(pInst, w, h, isMainCanvas); this.canvas = this.elt = elt || document.createElement('canvas'); if (isMainCanvas) { // for pixel method sharing with pimage this._pInst._curElement = this; this._pInst.canvas = this.canvas; } else { // hide if offscreen buffer by default this.canvas.style.display = 'none'; } this.elt.id = 'defaultCanvas0'; this.elt.classList.add('p5Canvas'); // Extend renderer with methods of p5.Element with getters for (const p of Object.getOwnPropertyNames(Element.prototype)) { if (p !== 'constructor' && p[0] !== '_') { Object.defineProperty(this, p, { get() { return this.wrappedElt[p]; } }); } } // Set canvas size this.elt.width = w * this._pixelDensity; this.elt.height = h * this._pixelDensity; this.elt.style.width = `${w}px`; this.elt.style.height = `${h}px`; // Attach canvas element to DOM if (this._pInst._userNode) { // user input node case this._pInst._userNode.appendChild(this.elt); } else { //create main element if (document.getElementsByTagName('main').length === 0) { let m = document.createElement('main'); document.body.appendChild(m); } //append canvas to main document.getElementsByTagName('main')[0].appendChild(this.elt); } // Get and store drawing context this.drawingContext = this.canvas.getContext('2d', attributes); if(attributes.colorSpace === 'display-p3'){ this.states.colorMode = RGBHDR; } this.scale(this._pixelDensity, this._pixelDensity); // Set and return p5.Element this.wrappedElt = new Element(this.elt, this._pInst); this.clipPath = null; } get filterRenderer() { if (!this._filterRenderer) { this._filterRenderer = new FilterRenderer2D(this); } return this._filterRenderer; } remove(){ this.wrappedElt.remove(); this.wrappedElt = null; this.canvas = null; this.elt = null; } getFilterGraphicsLayer() { // create hidden webgl renderer if it doesn't exist if (!this.filterGraphicsLayer) { const pInst = this._pInst; // create secondary layer this.filterGraphicsLayer = new Graphics( this.width, this.height, WEBGL, pInst ); } if ( this.filterGraphicsLayer.width !== this.width || this.filterGraphicsLayer.height !== this.height ) { // Resize the graphics layer this.filterGraphicsLayer.resizeCanvas(this.width, this.height); } if ( this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() ) { this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); } return this.filterGraphicsLayer; } _applyDefaults() { this.states.setValue('_cachedFillStyle', undefined); this.states.setValue('_cachedStrokeStyle', undefined); this._cachedBlendMode = BLEND; this._setFill(_DEFAULT_FILL); this._setStroke(_DEFAULT_STROKE); this.drawingContext.lineCap = ROUND; this.drawingContext.font = 'normal 12px sans-serif'; } resize(w, h) { super.resize(w, h); // save canvas properties const props = {}; for (const key in this.drawingContext) { const val = this.drawingContext[key]; if (typeof val !== 'object' && typeof val !== 'function') { props[key] = val; } } this.canvas.width = w * this._pixelDensity; this.canvas.height = h * this._pixelDensity; this.canvas.style.width = `${w}px`; this.canvas.style.height = `${h}px`; this.drawingContext.scale( this._pixelDensity, this._pixelDensity ); // reset canvas properties for (const savedKey in props) { try { this.drawingContext[savedKey] = props[savedKey]; } catch (err) { // ignore read-only property errors } } } ////////////////////////////////////////////// // COLOR | Setting ////////////////////////////////////////////// background(...args) { this.push(); this.resetMatrix(); if (args[0] instanceof Image) { 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 { // create background rect const color = this._pInst.color(...args); // Add accessible outputs if the method exists; on success, // set the accessible output background to white. if (this._pInst._addAccsOutput?.()) { this._pInst._accsBackground?.(color._getRGBA([255, 255, 255, 255])); } const newFill = color.toString(); this._setFill(newFill); if (this._isErasing) { this.blendMode(this._cachedBlendMode); } this.drawingContext.fillRect(0, 0, this.width, this.height); if (this._isErasing) { this._pInst.erase(); } } this.pop(); } clear() { this.drawingContext.save(); this.resetMatrix(); this.drawingContext.clearRect(0, 0, this.width, this.height); this.drawingContext.restore(); } fill(...args) { super.fill(...args); const color = this.states.fillColor; this._setFill(color.toString()); // Add accessible outputs if the method exists; on success, // set the accessible output background to white. if (this._pInst._addAccsOutput?.()) { this._pInst._accsCanvasColors?.('fill', color._getRGBA([255, 255, 255, 255])); } } stroke(...args) { super.stroke(...args); const color = this.states.strokeColor; this._setStroke(color.toString()); // Add accessible outputs if the method exists; on success, // set the accessible output background to white. if (this._pInst._addAccsOutput?.()) { this._pInst._accsCanvasColors?.('stroke', color._getRGBA([255, 255, 255, 255])); } } erase(opacityFill, opacityStroke) { if (!this._isErasing) { // cache the fill style this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle); const newFill = this._pInst.color(255, opacityFill).toString(); this.drawingContext.fillStyle = newFill; // cache the stroke style this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle); const newStroke = this._pInst.color(255, opacityStroke).toString(); this.drawingContext.strokeStyle = newStroke; // cache blendMode const tempBlendMode = this._cachedBlendMode; this.blendMode(REMOVE); this._cachedBlendMode = tempBlendMode; this._isErasing = true; } } noErase() { if (this._isErasing) { this.drawingContext.fillStyle = this.states._cachedFillStyle; this.drawingContext.strokeStyle = this.states._cachedStrokeStyle; this.blendMode(this._cachedBlendMode); this._isErasing = false; } } drawShape(shape) { const visitor = new PrimitiveToPath2DConverter({ strokeWeight: this.states.strokeWeight }); shape.accept(visitor); if (this._clipping) { const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(visitor.path, relativeTransform); this.clipPath.closePath(); } else { if (this.states.fillColor) { this.drawingContext.fill(visitor.path); } if (this.states.strokeColor) { this.drawingContext.stroke(visitor.path); } } } beginClip(options = {}) { super.beginClip(options); this._clipBaseTransform = this.drawingContext.getTransform(); // cache the fill style this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle); const newFill = this._pInst.color(255, 0).toString(); this.drawingContext.fillStyle = newFill; // cache the stroke style this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle); const newStroke = this._pInst.color(255, 0).toString(); this.drawingContext.strokeStyle = newStroke; // cache blendMode const tempBlendMode = this._cachedBlendMode; this.blendMode(BLEND); this._cachedBlendMode = tempBlendMode; // Since everything must be in one path, create a new single Path2D to chain all shapes onto. // Start a new path. Everything from here on out should become part of this // one path so that we can clip to the whole thing. this.clipPath = new Path2D(); this._clipBaseTransform = this.drawingContext.getTransform(); if (this._clipInvert) { // Slight hack: draw a big rectangle over everything with reverse winding // order. This is hopefully large enough to cover most things. this.clipPath.moveTo( -2 * this.width, -2 * this.height ); this.clipPath.lineTo( -2 * this.width, 2 * this.height ); this.clipPath.lineTo( 2 * this.width, 2 * this.height ); this.clipPath.lineTo( 2 * this.width, -2 * this.height ); this.clipPath.closePath(); } } endClip() { const savedTransform = this.drawingContext.getTransform(); this.drawingContext.setTransform(this._clipBaseTransform); this.drawingContext.clip(this.clipPath); this.drawingContext.setTransform(savedTransform); this.clipPath = null; super.endClip(); this.drawingContext.fillStyle = this.states._cachedFillStyle; this.drawingContext.strokeStyle = this.states._cachedStrokeStyle; this.blendMode(this._cachedBlendMode); } ////////////////////////////////////////////// // IMAGE | Loading & Displaying ////////////////////////////////////////////// image( img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight ) { let cnv; if (img.gifProperties) { img._animateGif(this._pInst); } try { if (img instanceof MediaElement) { img._ensureCanvas(); } if (this.states.tint && img.canvas) { cnv = this._getTintedImageCanvas(img); } if (!cnv) { cnv = img.canvas || img.elt; } let s = 1; if (img.width && img.width > 0) { s = cnv.width / img.width; } if (this._isErasing) { this.blendMode(this._cachedBlendMode); } this.drawingContext.drawImage( cnv, s * sx, s * sy, s * sWidth, s * sHeight, dx, dy, dWidth, dHeight ); if (this._isErasing) { this._pInst.erase(); } } catch (e) { if (e.name !== 'NS_ERROR_NOT_AVAILABLE') { throw e; } } } _getTintedImageCanvas(img) { if (!img.canvas) { return img; } 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; } // 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.states.tint[0] < 255 || this.states.tint[1] < 255 || this.states.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.states.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.states.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.states.tint[3] / 255; ctx.drawImage(img.canvas, 0, 0); } ctx.restore(); return img.tintCanvas; } ////////////////////////////////////////////// // IMAGE | Pixels ////////////////////////////////////////////// blendMode(mode) { if (mode === SUBTRACT) { console.warn('blendMode(SUBTRACT) only works in WEBGL mode.'); } else if ( mode === BLEND || mode === REMOVE || mode === DARKEST || mode === LIGHTEST || mode === DIFFERENCE || mode === MULTIPLY || mode === EXCLUSION || mode === SCREEN || mode === REPLACE || mode === OVERLAY || mode === HARD_LIGHT || mode === SOFT_LIGHT || mode === DODGE || mode === BURN || mode === ADD ) { this._cachedBlendMode = mode; this.drawingContext.globalCompositeOperation = mode; } else { throw new Error(`Mode ${mode} not recognized.`); } } blend(...args) { const currBlend = this.drawingContext.globalCompositeOperation; const blendMode = args[args.length - 1]; const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1); this.drawingContext.globalCompositeOperation = blendMode; p5$2.prototype.copy.apply(this, copyArgs); this.drawingContext.globalCompositeOperation = currBlend; } // p5.Renderer2D.prototype.get = p5.Renderer.prototype.get; // .get() is not overridden // x,y are canvas-relative (pre-scaled by _pixelDensity) _getPixel(x, y) { let imageData, index; imageData = this.drawingContext.getImageData(x, y, 1, 1).data; index = 0; return [ imageData[index + 0], imageData[index + 1], imageData[index + 2], imageData[index + 3] ]; } loadPixels() { const pd = this._pixelDensity; const w = this.width * pd; const h = this.height * pd; const imageData = this.drawingContext.getImageData(0, 0, w, h); // @todo this should actually set pixels per object, so diff buffers can // have diff pixel arrays. this.imageData = imageData; this.pixels = imageData.data; } set(x, y, imgOrCol) { // round down to get integer numbers x = Math.floor(x); y = Math.floor(y); if (imgOrCol instanceof Graphics || imgOrCol instanceof Image) { this.drawingContext.save(); this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); this.drawingContext.scale( this._pixelDensity, this._pixelDensity ); const width = imgOrCol.width; const height = imgOrCol.height; this.drawingContext.clearRect(x, y, width, height); this.drawingContext.drawImage(imgOrCol.canvas, x, y, width, height); } else { let r = 0, g = 0, b = 0, a = 0; let idx = 4 * (y * this._pixelDensity * (this.width * this._pixelDensity) + x * this._pixelDensity); if (!this.imageData) { this.loadPixels(); } if (typeof imgOrCol === 'number') { if (idx < this.pixels.length) { r = imgOrCol; g = imgOrCol; b = imgOrCol; a = 255; //this.updatePixels.call(this); } } else if (Array.isArray(imgOrCol)) { if (imgOrCol.length < 4) { throw new Error('pixel array must be of the form [R, G, B, A]'); } if (idx < this.pixels.length) { r = imgOrCol[0]; g = imgOrCol[1]; b = imgOrCol[2]; a = imgOrCol[3]; //this.updatePixels.call(this); } } else if (imgOrCol instanceof p5$2.Color) { if (idx < this.pixels.length) { [r, g, b, a] = imgOrCol._getRGBA([255, 255, 255, 255]); //this.updatePixels.call(this); } } // loop over pixelDensity * pixelDensity for (let i = 0; i < this._pixelDensity; i++) { for (let j = 0; j < this._pixelDensity; j++) { // loop over idx = 4 * ((y * this._pixelDensity + j) * this.width * this._pixelDensity + (x * this._pixelDensity + i)); this.pixels[idx] = r; this.pixels[idx + 1] = g; this.pixels[idx + 2] = b; this.pixels[idx + 3] = a; } } } } updatePixels(x, y, w, h) { const pd = this._pixelDensity; if ( x === undefined && y === undefined && w === undefined && h === undefined ) { x = 0; y = 0; w = this.width; h = this.height; } x *= pd; y *= pd; w *= pd; h *= pd; if (this.gifProperties) { this.gifProperties.frames[this.gifProperties.displayIndex].image = this.imageData; } this.drawingContext.putImageData(this.imageData, 0, 0, x, y, w, h); } ////////////////////////////////////////////// // SHAPE | 2D Primitives ////////////////////////////////////////////// /* * This function requires that: * * 0 <= start < TWO_PI * * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { const ctx = this.drawingContext; const centerX = x + w / 2, centerY = y + h / 2, radiusX = w / 2, radiusY = h / 2; if (this._clipping) { const tempPath = new Path2D(); tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(tempPath, relativeTransform); return this; } // Determines whether to add a line to the center, which should be done // when the mode is PIE or default; as well as when the start and end // angles do not form a full circle. const createPieSlice = ! ( mode === CHORD || mode === OPEN || (stop - start) % TWO_PI === 0 ); // Fill curves if (this.states.fillColor) { ctx.beginPath(); ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); if (createPieSlice) ctx.lineTo(centerX, centerY); ctx.closePath(); ctx.fill(); } // Stroke curves if (this.states.strokeColor) { ctx.beginPath(); ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); if (mode === PIE && createPieSlice) { // In PIE mode, stroke is added to the center and back to path, // unless the pie forms a complete ellipse (see: createPieSlice) ctx.lineTo(centerX, centerY); } if (mode === PIE || mode === CHORD) { // Stroke connects back to path begin for both PIE and CHORD ctx.closePath(); } ctx.stroke(); } return this; } ellipse(args) { const ctx = this.drawingContext; const doFill = !!this.states.fillColor, doStroke = this.states.strokeColor; const x = parseFloat(args[0]), y = parseFloat(args[1]), w = parseFloat(args[2]), h = parseFloat(args[3]); if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; } } else if (!doFill && doStroke) { if (this._getStroke() === styleEmpty) { return this; } } const centerX = x + w / 2, centerY = y + h / 2, radiusX = w / 2, radiusY = h / 2; if (this._clipping) { const tempPath = new Path2D(); tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(tempPath, relativeTransform); return this; } ctx.beginPath(); ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); ctx.closePath(); if (doFill) { ctx.fill(); } if (doStroke) { ctx.stroke(); } return this; } line(x1, y1, x2, y2) { const ctx = this.drawingContext; if (!this.states.strokeColor) { return this; } else if (this._getStroke() === styleEmpty) { return this; } if (this._clipping) { const tempPath = new Path2D(); tempPath.moveTo(x1, y1); tempPath.lineTo(x2, y2); const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(tempPath, relativeTransform); return this; } ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); return this; } point(x, y) { const ctx = this.drawingContext; if (!this.states.strokeColor) { return this; } else if (this._getStroke() === styleEmpty) { return this; } const s = this._getStroke(); const f = this._getFill(); if (this._clipping) { const tempPath = new Path2D(); const drawingContextWidth = this.drawingContext.lineWidth; tempPath.arc(x, y, drawingContextWidth / 2, 0, TWO_PI); const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(tempPath, relativeTransform); return this; } this._setFill(s); ctx.beginPath(); ctx.arc(x, y, ctx.lineWidth / 2, 0, TWO_PI, false); ctx.fill(); this._setFill(f); return this; } quad(x1, y1, x2, y2, x3, y3, x4, y4) { const ctx = this.drawingContext; const doFill = !!this.states.fillColor, doStroke = this.states.strokeColor; if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; } } else if (!doFill && doStroke) { if (this._getStroke() === styleEmpty) { return this; } } if (this._clipping) { const tempPath = new Path2D(); tempPath.moveTo(x1, y1); tempPath.lineTo(x2, y2); tempPath.lineTo(x3, y3); tempPath.lineTo(x4, y4); tempPath.closePath(); const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(tempPath, relativeTransform); return this; } ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x4, y4); ctx.closePath(); if (doFill) { ctx.fill(); } if (doStroke) { ctx.stroke(); } return this; } rect(args) { const x = args[0]; const y = args[1]; const w = args[2]; const h = args[3]; let tl = args[4]; let tr = args[5]; let br = args[6]; let bl = args[7]; const ctx = this.drawingContext; const doFill = !!this.states.fillColor, doStroke = this.states.strokeColor; if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; } } else if (!doFill && doStroke) { if (this._getStroke() === styleEmpty) { return this; } } if (this._clipping) { const tempPath = new Path2D(); if (typeof tl === 'undefined') { tempPath.rect(x, y, w, h); } else { tempPath.roundRect(x, y, w, h, [tl, tr, br, bl]); } const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(tempPath, relativeTransform); return this; } ctx.beginPath(); if (typeof tl === 'undefined') { // No rounded corners ctx.rect(x, y, w, h); } else { // At least one rounded corner // Set defaults when not specified if (typeof tr === 'undefined') { tr = tl; } if (typeof br === 'undefined') { br = tr; } if (typeof bl === 'undefined') { bl = br; } // corner rounding must always be positive const absW = Math.abs(w); const absH = Math.abs(h); const hw = absW / 2; const hh = absH / 2; // Clip radii if (absW < 2 * tl) { tl = hw; } if (absH < 2 * tl) { tl = hh; } if (absW < 2 * tr) { tr = hw; } if (absH < 2 * tr) { tr = hh; } if (absW < 2 * br) { br = hw; } if (absH < 2 * br) { br = hh; } if (absW < 2 * bl) { bl = hw; } if (absH < 2 * bl) { bl = hh; } ctx.roundRect(x, y, w, h, [tl, tr, br, bl]); } if (doFill) { ctx.fill(); } if (doStroke) { ctx.stroke(); } return this; } triangle(args) { const ctx = this.drawingContext; const doFill = !!this.states.fillColor, doStroke = this.states.strokeColor; const x1 = args[0], y1 = args[1]; const x2 = args[2], y2 = args[3]; const x3 = args[4], y3 = args[5]; if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; } } else if (!doFill && doStroke) { if (this._getStroke() === styleEmpty) { return this; } } if (this._clipping) { const tempPath = new Path2D(); tempPath.moveTo(x1, y1); tempPath.lineTo(x2, y2); tempPath.lineTo(x3, y3); tempPath.closePath(); const currentTransform = this.drawingContext.getTransform(); const clipBaseTransform = this._clipBaseTransform.inverse(); const relativeTransform = clipBaseTransform.multiply(currentTransform); this.clipPath.addPath(tempPath, relativeTransform); return this; } ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.closePath(); if (doFill) { ctx.fill(); } if (doStroke) { ctx.stroke(); } return this; } ////////////////////////////////////////////// // SHAPE | Attributes ////////////////////////////////////////////// strokeCap(cap) { if ( cap === ROUND || cap === SQUARE || cap === PROJECT ) { this.drawingContext.lineCap = cap; } return this; } strokeJoin(join) { if ( join === ROUND || join === BEVEL || join === MITER ) { this.drawingContext.lineJoin = join; } return this; } strokeWeight(w) { super.strokeWeight(w); if (typeof w === 'undefined' || w === 0) { // hack because lineWidth 0 doesn't work this.drawingContext.lineWidth = 0.0001; } else { this.drawingContext.lineWidth = w; } return this; } _getFill() { if (!this.states._cachedFillStyle) { this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle); } return this.states._cachedFillStyle; } _setFill(fillStyle) { if (fillStyle !== this.states._cachedFillStyle) { this.drawingContext.fillStyle = fillStyle; this.states.setValue('_cachedFillStyle', fillStyle); } } _getStroke() { if (!this.states._cachedStrokeStyle) { this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle); } return this.states._cachedStrokeStyle; } _setStroke(strokeStyle) { if (strokeStyle !== this.states._cachedStrokeStyle) { this.drawingContext.strokeStyle = strokeStyle; this.states.setValue('_cachedStrokeStyle', strokeStyle); } } ////////////////////////////////////////////// // TRANSFORM ////////////////////////////////////////////// applyMatrix(a, b, c, d, e, f) { this.drawingContext.transform(a, b, c, d, e, f); } getWorldToScreenMatrix() { let domMatrix = new DOMMatrix() .scale(1 / this._pixelDensity) .multiply(this.drawingContext.getTransform()); return new Matrix(domMatrix.toFloat32Array()); } resetMatrix() { this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); this.drawingContext.scale( this._pixelDensity, this._pixelDensity ); return this; } rotate(rad) { this.drawingContext.rotate(rad); } scale(x, y) { this.drawingContext.scale(x, y); return this; } translate(x, y) { // support passing a vector as the 1st parameter if (x instanceof p5$2.Vector) { y = x.y; x = x.x; } this.drawingContext.translate(x, y); return this; } ////////////////////////////////////////////// // TYPOGRAPHY (see src/type/textCore.js) ////////////////////////////////////////////// ////////////////////////////////////////////// // STRUCTURE ////////////////////////////////////////////// // a push() operation is in progress. // the renderer should return a 'style' object that it wishes to // store on the push stack. // derived renderers should call the base class' push() method // to fetch the base style object. push() { this.drawingContext.save(); // get the base renderer style return super.push(); } // a pop() operation is in progress // the renderer is passed the 'style' object that it returned // from its push() method. // derived renderers should pass this object to their base // class' pop method pop(style) { this.drawingContext.restore(); super.pop(style); } // Text support methods textCanvas() { return this.canvas; } textDrawingContext() { return this.drawingContext; } _renderText(text, x, y, maxY, minY) { let states = this.states; let context = this.textDrawingContext(); if (y < minY || y >= maxY) { return; // don't render lines beyond minY/maxY } this.push(); // no stroke unless specified by user if (states.strokeColor && states.strokeSet) { context.strokeText(text, x, y); } if (!this._clipping && states.fillColor) { // if fill hasn't been set by user, use default text fill if (!states.fillSet) { this._setFill(DefaultFill); } context.fillText(text, x, y); } this.pop(); } /* Position the lines of text based on their textAlign/textBaseline properties */ _positionLines(x, y, width, height, lines) { let { textLeading, textAlign } = this.states; let adjustedX, lineData = new Array(lines.length); let adjustedW = typeof width === 'undefined' ? 0 : width; let adjustedH = typeof height === 'undefined' ? 0 : height; for (let i = 0; i < lines.length; i++) { switch (textAlign) { case textCoreConstants.START: throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT case LEFT: adjustedX = x; break; case CENTER: adjustedX = x + adjustedW / 2; break; case RIGHT: adjustedX = x + adjustedW; break; case textCoreConstants.END: throw new Error('textBounds: END not yet supported for textAlign'); } lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; } return this._yAlignOffset(lineData, adjustedH); } /* Get the y-offset for text given the height, leading, line-count and textBaseline property */ _yAlignOffset(dataArr, height) { if (typeof height === 'undefined') { throw Error('_yAlignOffset: height is required'); } let { textLeading, textBaseline } = this.states; let yOff = 0, numLines = dataArr.length; let ydiff = height - (textLeading * (numLines - 1)); switch (textBaseline) { // drawingContext ? case TOP: break; // ?? case BASELINE: break; case textCoreConstants._CTX_MIDDLE: yOff = ydiff / 2 + this._middleAlignOffset(); break; case BOTTOM: yOff = ydiff; break; case textCoreConstants.IDEOGRAPHIC: console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES? break; case textCoreConstants.HANGING: console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES? break; } dataArr.forEach(ele => ele.y += yOff); return dataArr; } } function renderer2D(p5, fn){ /** * p5.Renderer2D * The 2D graphics canvas renderer class. * extends p5.Renderer * @private */ p5.Renderer2D = Renderer2D; p5.renderers[P2D] = Renderer2D; p5.renderers['p2d-hdr'] = new Proxy(Renderer2D, { construct(target, [pInst, w, h, isMainCanvas, elt]){ return new target(pInst, w, h, isMainCanvas, elt, { colorSpace: 'display-p3' }); } }); } /** * @module Structure * @submodule Structure * @for p5 * @requires constants */ /** * This is the p5 instance constructor. * * A p5 instance holds all the properties and methods related to * a p5 sketch. It expects an incoming sketch closure and it can also * take an optional node parameter for attaching the generated p5 canvas * to a node. The sketch closure takes the newly created p5 instance as * its sole argument and may optionally set an asynchronous function * using `async/await`, along with the standard setup(), * and/or setup(), and/or draw() * properties on it for running a sketch. * * A p5 sketch can run in "global" or "instance" mode: * "global" - all properties and methods are attached to the window * "instance" - all properties and methods are bound to this p5 object * * @class p5 * @param {function(p5)} sketch a closure that can set optional preload(), * setup(), and/or draw() properties on the * given p5 instance * @param {String|HTMLElement} [node] element to attach canvas to * @return {p5} a p5 instance */ let p5$2 = class p5 { static VERSION = VERSION; // This is a pointer to our global mode p5 instance, if we're in // global mode. static instance = null; static lifecycleHooks = { presetup: [], postsetup: [], predraw: [], postdraw: [], remove: [] }; // FES stub static _checkForUserDefinedFunctions = () => {}; static _friendlyFileLoadError = () => {}; constructor(sketch, node) { // Apply addon defined decorations if(p5.decorations.size > 0){ for (const [patternArray, decoration] of p5.decorations) { for(const member in p5.prototype) { // Member must be a function if (typeof p5.prototype[member] !== 'function') continue; if (!patternArray.some(pattern => { if (typeof pattern === 'string') { return pattern === member; } else if (pattern instanceof RegExp) { return pattern.test(member); } })) continue; p5.prototype[member] = decoration(p5.prototype[member], { kind: 'method', name: member, access: {}, static: false, private: false, addInitializer(initializer){} }); } } p5.decorations.clear(); } ////////////////////////////////////////////// // PRIVATE p5 PROPERTIES AND METHODS ////////////////////////////////////////////// this.hitCriticalError = false; this._setupDone = false; this._userNode = node; this._curElement = null; this._elements = []; this._glAttributes = null; this._webgpuAttributes = null; this._requestAnimId = 0; this._isGlobal = false; this._loop = true; this._startListener = null; this._initializeInstanceVariables(); this._events = { }; this._removeAbortController = new AbortController(); this._removeSignal = this._removeAbortController.signal; this._millisStart = -1; this._recording = false; // States used in the custom random generators this._lcg_random_state = null; // NOTE: move to random.js this._gaussian_previous = false; // NOTE: move to random.js // ensure correct reporting of window dimensions this._updateWindowSize(); const bindGlobal = createBindGlobal(this); // If the user has created a global setup or draw function, // assume "global" mode and make everything global (i.e. on the window) if (!sketch) { this._isGlobal = true; if (window.hitCriticalError) { return; } p5.instance = this; // Loop through methods on the prototype and attach them to the window // All methods and properties with name starting with '_' will be skipped for (const p of Object.getOwnPropertyNames(p5.prototype)) { if(p[0] === '_') continue; bindGlobal(p); } const protectedProperties = ['constructor', 'length']; // Attach its properties to the window for (const p in this) { if (this.hasOwnProperty(p)) { if(p[0] === '_' || protectedProperties.includes(p)) continue; bindGlobal(p); } } } else { // Else, the user has passed in a sketch closure that may set // user-provided 'setup', 'draw', etc. properties on this instance of p5 sketch(this); // Run a check to see if the user has misspelled 'setup', 'draw', etc // detects capitalization mistakes only ( Setup, SETUP, MouseClicked, etc) p5._checkForUserDefinedFunctions(this); } const focusHandler = () => { this.focused = true; }; const blurHandler = () => { this.focused = false; }; window.addEventListener('focus', focusHandler); window.addEventListener('blur', blurHandler); p5.lifecycleHooks.remove.push(function() { window.removeEventListener('focus', focusHandler); window.removeEventListener('blur', blurHandler); }); // Initialization complete, start runtime if (document.readyState === 'complete') { this.#_start(); } else { this._startListener = this.#_start.bind(this); window.addEventListener('load', this._startListener, false); } } get pixels(){ return this._renderer.pixels; } get drawingContext(){ return this._renderer.drawingContext; } static _registeredAddons = new Set(); static registerAddon(addon) { const lifecycles = {}; // Don't re-register an addon. This allows addons // to register dependency addons without worrying about // them getting double-added. if (p5._registeredAddons.has(addon)) return; p5._registeredAddons.add(addon); addon(p5, p5.prototype, lifecycles); const validLifecycles = Object.keys(p5.lifecycleHooks); for(const name of validLifecycles){ if(typeof lifecycles[name] === 'function'){ p5.lifecycleHooks[name].push(lifecycles[name]); } } } static decorations = new Map(); static decorateHelper(pattern, decoration){ let patternArray = pattern; if (!Array.isArray(pattern)) patternArray = [pattern]; p5.decorations.set(patternArray, decoration); } #customActions = {}; _customActions = new Proxy({}, { get: (target, prop) => { if(!this.#customActions[prop]){ const context = this._isGlobal ? window : this; if(typeof context[prop] === 'function'){ this.#customActions[prop] = context[prop].bind(this); } } return this.#customActions[prop]; } }); async #_start() { if (this.hitCriticalError) return; // Find node if id given if (this._userNode) { if (typeof this._userNode === 'string') { this._userNode = document.getElementById(this._userNode); } } await this.#_setup(); if (this.hitCriticalError) return; if (!this._recording) { this._draw(); } } async #_setup() { // Run `presetup` hooks await this._runLifecycleHook('presetup'); if (this.hitCriticalError) return; // Always create a default canvas. // Later on if the user calls createCanvas, this default one // will be replaced this.createCanvas( 100, 100, P2D ); // Record the time when setup starts. millis() will start at 0 within // setup, but this isn't documented, locked-in behavior yet. this._millisStart = window.performance.now(); const context = this._isGlobal ? window : this; if (typeof context.setup === 'function') { await context.setup(); } if (this.hitCriticalError) return; const canvases = document.getElementsByTagName('canvas'); for (const k of canvases) { // Apply touchAction = 'none' to canvases to prevent scrolling // when dragging on canvas elements k.style.touchAction = 'none'; // unhide any hidden canvases that were created if (k.dataset.hidden === 'true') { k.style.visibility = ''; delete k.dataset.hidden; } } this._lastTargetFrameTime = window.performance.now(); this._lastRealFrameTime = window.performance.now(); this._setupDone = true; if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._updateAccsOutput(); } // Run `postsetup` hooks await this._runLifecycleHook('postsetup'); // Record the time when the draw loop starts so that millis() starts at 0 // when the draw loop begins. this._millisStart = window.performance.now(); } // While '#_draw' here is async, it is not awaited as 'requestAnimationFrame' // does not await its callback. Thus it is not recommended for 'draw()` to be // async and use await within as the next frame may start rendering before the // current frame finish awaiting. The same goes for lifecycle hooks 'predraw' // and 'postdraw'. async _draw(requestAnimationFrameTimestamp) { if (this.hitCriticalError) return; const now = requestAnimationFrameTimestamp || window.performance.now(); const timeSinceLastFrame = now - this._lastTargetFrameTime; const targetTimeBetweenFrames = 1000 / this._targetFrameRate; // only draw if we really need to; don't overextend the browser. // draw if we're within 5ms of when our next frame should paint // (this will prevent us from giving up opportunities to draw // again when it's really about time for us to do so). fixes an // issue where the frameRate is too low if our refresh loop isn't // in sync with the browser. note that we have to draw once even // if looping is off, so we bypass the time delay if that // is the case. const epsilon = 5; if ( !this._loop || timeSinceLastFrame >= targetTimeBetweenFrames - epsilon ) { //mandatory update values(matrixes and stack) this.deltaTime = now - this._lastRealFrameTime; this._frameRate = 1000.0 / this.deltaTime; await this.redraw(); this._lastTargetFrameTime = Math.max(this._lastTargetFrameTime + targetTimeBetweenFrames, now); this._lastRealFrameTime = now; // If the user is actually using mouse module, then update // coordinates, otherwise skip. We can test this by simply // checking if any of the mouse functions are available or not. // NOTE : This reflects only in complete build or modular build. if (typeof this._updateMouseCoords !== 'undefined') { this._updateMouseCoords(); //reset delta values so they reset even if there is no mouse event to set them // for example if the mouse is outside the screen this.movedX = 0; this.movedY = 0; } } // get notified the next time the browser gives us // an opportunity to draw. if (this._loop) { this._requestAnimId = window.requestAnimationFrame( this._draw.bind(this) ); } } /** * Removes the sketch from the web page. * * Calling `remove()` stops the draw loop and removes any HTML elements * created by the sketch, including the canvas. A new sketch can be * created by using the p5() constructor, as in * `new p5()`. * * @example * // Double-click to remove the canvas. * * function setup() { * createCanvas(100, 100); * * describe( * 'A white circle on a gray background. The circle follows the mouse as the user moves. The sketch disappears when the user double-clicks.' * ); * } * * function draw() { * // Paint the background repeatedly. * background(200); * * // Draw circles repeatedly. * circle(mouseX, mouseY, 40); * } * * // Remove the sketch when the user double-clicks. * function doubleClicked() { * remove(); * } */ async remove() { // Remove start listener to prevent orphan canvas being created if(this._startListener){ window.removeEventListener('load', this._startListener, false); } if (this._curElement) { // stop draw this._loop = false; if (this._requestAnimId) { window.cancelAnimationFrame(this._requestAnimId); } // Send sketch remove signal this._removeAbortController.abort(); // remove DOM elements created by p5 for (const e of this._elements) { if (e.elt && e.elt.parentNode) { e.elt.parentNode.removeChild(e.elt); } } // Run `remove` hooks await this._runLifecycleHook('remove'); } // remove window bound properties and methods if (this._isGlobal) { for (const p in p5.prototype) { try { delete window[p]; } catch (x) { window[p] = undefined; } } for (const p2 in this) { if (this.hasOwnProperty(p2)) { try { delete window[p2]; } catch (x) { window[p2] = undefined; } } } p5.instance = null; } } async _runLifecycleHook(hookName) { await Promise.all(p5.lifecycleHooks[hookName].map(hook => { return hook.call(this); })); } _initializeInstanceVariables() { this._accessibleOutputs = { text: false, grid: false, textLabel: false, gridLabel: false }; this._styles = []; this._downKeys = {}; //Holds the key codes of currently pressed keys this._downKeyCodes = {}; } }; // Global helper function for binding properties to window in global mode function createBindGlobal(instance) { return function bindGlobal(property) { if (property === 'constructor') return; // Check if this property has a getter on the instance or prototype const instanceDescriptor = Object.getOwnPropertyDescriptor( instance, property ); const prototypeDescriptor = Object.getOwnPropertyDescriptor( p5$2.prototype, property ); const hasGetter = (instanceDescriptor && instanceDescriptor.get) || (prototypeDescriptor && prototypeDescriptor.get); // Only check if it's a function if it doesn't have a getter // to avoid actually evaluating getters before things like the // renderer are fully constructed let isPrototypeFunction = false; let isConstant = false; let constantValue; if (!hasGetter) { const prototypeValue = p5$2.prototype[property]; isPrototypeFunction = typeof prototypeValue === 'function'; // Check if this is a true constant from the constants module if (!isPrototypeFunction && constants[property] !== undefined) { isConstant = true; constantValue = prototypeValue; } } if (isPrototypeFunction) { // For regular functions, cache the bound function const boundFunction = p5$2.prototype[property].bind(instance); Object.defineProperty(window, property, { configurable: true, enumerable: true, value: boundFunction }); } else if (isConstant) { // For constants, cache the value directly Object.defineProperty(window, property, { configurable: true, enumerable: true, value: constantValue }); } else if (hasGetter || !isPrototypeFunction) { // For properties with getters or non-function properties, use lazy optimization // On first access, determine the type and optimize subsequent accesses let lastFunction = null; let boundFunction = null; let isFunction = null; // null = unknown, true = function, false = not function Object.defineProperty(window, property, { configurable: true, enumerable: true, get: () => { const currentValue = instance[property]; if (isFunction === null) { // First access - determine type and optimize isFunction = typeof currentValue === 'function'; if (isFunction) { lastFunction = currentValue; boundFunction = currentValue.bind(instance); return boundFunction; } else { return currentValue; } } else if (isFunction) { // Optimized function path - only rebind if function changed if (currentValue !== lastFunction) { lastFunction = currentValue; boundFunction = currentValue.bind(instance); } return boundFunction; } else { // Optimized non-function path return currentValue; } } }); } }; } // Attach constants to p5 prototype for (const k in constants) { p5$2.prototype[k] = constants[k]; } p5$2.registerAddon(transform$1); p5$2.registerAddon(structure); p5$2.registerAddon(environment$1); p5$2.registerAddon(rendering); p5$2.registerAddon(renderer); p5$2.registerAddon(renderer2D); p5$2.registerAddon(graphics); ////////////////////////////////////////////// // PUBLIC p5 PROPERTIES AND METHODS ////////////////////////////////////////////// /** * A function that's called once when the sketch begins running. * * Declaring the function `setup()` sets a code block to run once * automatically when the sketch starts running. It's used to perform * setup tasks such as creating the canvas and initializing variables: * * ```js * function setup() { * // Code to run once at the start of the sketch. * } * ``` * * Code placed in `setup()` will run once before code placed in * draw() begins looping. * If `setup()` is declared `async` (e.g. `async function setup()`), * execution pauses at each `await` until its promise resolves. * For example, `font = await loadFont(...)` waits for the font asset * to load because `loadFont()` function returns a promise, and the await * keyword means the program will wait for the promise to resolve. * This ensures that all assets are fully loaded before the sketch continues. * * * loading assets. * * Note: `setup()` doesn’t have to be declared, but it’s common practice to do so. * * @method setup * @for p5 * * @example * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the circle. * circle(50, 50, 40); * * describe('A white circle on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * * // Paint the background once. * background(200); * * describe( * 'A white circle on a gray background. The circle follows the mouse as the user moves, leaving a trail.' * ); * } * * function draw() { * // Draw circles repeatedly. * circle(mouseX, mouseY, 40); * } * * @example * let img; * * async function setup() { * img = await loadImage('assets/bricks.jpg'); * * createCanvas(100, 100); * * // Draw the image. * image(img, 0, 0); * * describe( * 'A white circle on a brick wall. The circle follows the mouse as the user moves, leaving a trail.' * ); * } * * function draw() { * // Style the circle. * noStroke(); * * // Draw the circle. * circle(mouseX, mouseY, 10); * } */ /** * A function that's called repeatedly while the sketch runs. * * Declaring the function `draw()` sets a code block to run repeatedly * once the sketch starts. It’s used to create animations and respond to * user inputs: * * ```js * function draw() { * // Code to run repeatedly. * } * ``` * * This is often called the "draw loop" because p5.js calls the code in * `draw()` in a loop behind the scenes. By default, `draw()` tries to run * 60 times per second. The actual rate depends on many factors. The * drawing rate, called the "frame rate", can be controlled by calling * frameRate(). The number of times `draw()` * has run is stored in the system variable * frameCount(). * * Code placed within `draw()` begins looping after * setup() runs. `draw()` will run until the user * closes the sketch. `draw()` can be stopped by calling the * noLoop() function. `draw()` can be resumed by * calling the loop() function. * * @method draw * @for p5 * * @example * function setup() { * createCanvas(100, 100); * * // Paint the background once. * background(200); * * describe( * 'A white circle on a gray background. The circle follows the mouse as the user moves, leaving a trail.' * ); * } * * function draw() { * // Draw circles repeatedly. * circle(mouseX, mouseY, 40); * } * * @example * function setup() { * createCanvas(100, 100); * * describe( * 'A white circle on a gray background. The circle follows the mouse as the user moves.' * ); * } * * function draw() { * // Paint the background repeatedly. * background(200); * * // Draw circles repeatedly. * circle(mouseX, mouseY, 40); * } * * @example * // Double-click the canvas to change the circle's color. * * function setup() { * createCanvas(100, 100); * * describe( * 'A white circle on a gray background. The circle follows the mouse as the user moves. The circle changes color to pink when the user double-clicks.' * ); * } * * function draw() { * // Paint the background repeatedly. * background(200); * * // Draw circles repeatedly. * circle(mouseX, mouseY, 40); * } * * // Change the fill color when the user double-clicks. * function doubleClicked() { * fill('deeppink'); * } */ /** * Turns off the parts of the Friendly Error System (FES) that impact performance. * * The FES * can cause sketches to draw slowly because it does extra work behind the * scenes. For example, the FES checks the arguments passed to functions, * which takes time to process. Disabling the FES can significantly improve * performance by turning off these checks. * * @static * @property {Boolean} disableFriendlyErrors * * @example * // Disable the FES. * p5.disableFriendlyErrors = true; * * function setup() { * createCanvas(100, 100); * * background(200); * * // The circle() function requires three arguments. The * // next line would normally display a friendly error that * // points this out. Instead, nothing happens and it fails * // silently. * circle(50, 50); * * describe('A gray square.'); * } */ /** * Loads a p5.js library. * * A library is a function that adds functionality to p5.js by adding methods * and properties for sketches to use, or for automatically running code at * different stages of the p5.js lifecycle. Take a look at the * contributor docs for creating libraries * to learn more about creating libraries. * * @static * @method registerAddon * @param {Function} library The library function to register * * @example * function myAddon(p5, fn, lifecycles) { * fn.sayHello = function() { * this.textAlign(this.CENTER, this.CENTER); * this.text('Hello!', this.width / 2, this.height / 2); * }; * } * p5.registerAddon(myAddon); * * function setup() { * createCanvas(100, 100); * * background(200); * sayHello(); // The sayHello method is now available! * * describe('The text "Hello!"'); * } */ function shape(p5){ p5.registerAddon(primitives); p5.registerAddon(attributes); p5.registerAddon(curves); p5.registerAddon(vertex); p5.registerAddon(customShapes); } /** * @module Environment * @submodule Environment * @for p5 * @requires core */ function describe(p5, fn){ const descContainer = '_Description'; //Fallback container const fallbackDescId = '_fallbackDesc'; //Fallback description const fallbackTableId = '_fallbackTable'; //Fallback Table const fallbackTableElId = '_fte_'; //Fallback Table Element const labelContainer = '_Label'; //Label container const labelDescId = '_labelDesc'; //Label description const labelTableId = '_labelTable'; //Label Table const labelTableElId = '_lte_'; //Label Table Element /** * Creates a screen reader-accessible description of the canvas. * * The first parameter, `text`, is the description of the canvas. * * The second parameter, `display`, is optional. It determines how the * description is displayed. If `LABEL` is passed, as in * `describe('A description.', LABEL)`, the description will be visible in * a div element next to the canvas. If `FALLBACK` is passed, as in * `describe('A description.', FALLBACK)`, the description will only be * visible to screen readers. This is the default mode. * * Read * Writing accessible canvas descriptions * to learn more about making sketches accessible. * * @method describe * @param {String} text description of the canvas. * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * * @example * function setup() { * background('pink'); * * // Draw a heart. * fill('red'); * noStroke(); * circle(67, 67, 20); * circle(83, 67, 20); * triangle(91, 73, 75, 95, 59, 73); * * // Add a general description of the canvas. * describe('A pink square with a red heart in the bottom-right corner.'); * } * * @example * function setup() { * background('pink'); * * // Draw a heart. * fill('red'); * noStroke(); * circle(67, 67, 20); * circle(83, 67, 20); * triangle(91, 73, 75, 95, 59, 73); * * // Add a general description of the canvas * // and display it for debugging. * describe('A pink square with a red heart in the bottom-right corner.', LABEL); * } * * @example * function setup(){ * createCanvas(100, 100); * }; * * function draw() { * background(200); * * // The expression * // frameCount % 100 * // causes x to increase from 0 * // to 99, then restart from 0. * let x = frameCount % 100; * * // Draw the circle. * fill(0, 255, 0); * circle(x, 50, 40); * * // Add a general description of the canvas. * describe(`A green circle at (${x}, 50) moves from left to right on a gray square.`); * } * * @example * function setup(){ * createCanvas(100, 100); * } * * function draw() { * background(200); * * // The expression * // frameCount % 100 * // causes x to increase from 0 * // to 99, then restart from 0. * let x = frameCount % 100; * * // Draw the circle. * fill(0, 255, 0); * circle(x, 50, 40); * * // Add a general description of the canvas * // and display it for debugging. * describe(`A green circle at (${x}, 50) moves from left to right on a gray square.`, LABEL); * } */ fn.describe = function(text, display) { // p5._validateParameters('describe', arguments); if (typeof text !== 'string') { return; } const cnvId = this.canvas.id; //calls function that adds punctuation for better screen reading text = _descriptionText(text); //if there is no dummyDOM if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; } if (!this.descriptions) { this.descriptions = {}; } //check if html structure for description is ready if (this.descriptions.fallback) { //check if text is different from current description if (this.descriptions.fallback.innerHTML !== text) { //update description this.descriptions.fallback.innerHTML = text; } } else { //create fallback html structure this._describeHTML('fallback', text); } //if display is LABEL if (display === this.LABEL) { //check if html structure for label is ready if (this.descriptions.label) { //check if text is different from current label if (this.descriptions.label.innerHTML !== text) { //update label description this.descriptions.label.innerHTML = text; } } else { //create label html structure this._describeHTML('label', text); } } }; /** * Creates a screen reader-accessible description of elements in the canvas. * * Elements are shapes or groups of shapes that create meaning together. For * example, a few overlapping circles could make an "eye" element. * * The first parameter, `name`, is the name of the element. * * The second parameter, `text`, is the description of the element. * * The third parameter, `display`, is optional. It determines how the * description is displayed. If `LABEL` is passed, as in * `describe('A description.', LABEL)`, the description will be visible in * a div element next to the canvas. Using `LABEL` creates unhelpful * duplicates for screen readers. Only use `LABEL` during development. If * `FALLBACK` is passed, as in `describe('A description.', FALLBACK)`, the * description will only be visible to screen readers. This is the default * mode. * * Read * Writing accessible canvas descriptions * to learn more about making sketches accessible. * * @method describeElement * @param {String} name name of the element. * @param {String} text description of the element. * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * * @example * function setup() { * background('pink'); * * // Describe the first element * // and draw it. * describeElement('Circle', 'A yellow circle in the top-left corner.'); * noStroke(); * fill('yellow'); * circle(25, 25, 40); * * // Describe the second element * // and draw it. * describeElement('Heart', 'A red heart in the bottom-right corner.'); * fill('red'); * circle(66.6, 66.6, 20); * circle(83.2, 66.6, 20); * triangle(91.2, 72.6, 75, 95, 58.6, 72.6); * * // Add a general description of the canvas. * describe('A red heart and yellow circle over a pink background.'); * } * * @example * function setup() { * background('pink'); * * // Describe the first element * // and draw it. Display the * // description for debugging. * describeElement('Circle', 'A yellow circle in the top-left corner.', LABEL); * noStroke(); * fill('yellow'); * circle(25, 25, 40); * * // Describe the second element * // and draw it. Display the * // description for debugging. * describeElement('Heart', 'A red heart in the bottom-right corner.', LABEL); * fill('red'); * circle(66.6, 66.6, 20); * circle(83.2, 66.6, 20); * triangle(91.2, 72.6, 75, 95, 58.6, 72.6); * * // Add a general description of the canvas. * describe('A red heart and yellow circle over a pink background.'); * } */ fn.describeElement = function(name, text, display) { // p5._validateParameters('describeElement', arguments); if (typeof text !== 'string' || typeof name !== 'string') { return; } const cnvId = this.canvas.id; //calls function that adds punctuation for better screen reading text = _descriptionText(text); //calls function that adds punctuation for better screen reading let elementName = _elementName(name); //remove any special characters from name to use it as html id name = name.replace(/[^a-zA-Z0-9]/g, ''); //store element description let inner = `${elementName}${text}`; //if there is no dummyDOM if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; } if (!this.descriptions) { this.descriptions = { fallbackElements: {} }; } else if (!this.descriptions.fallbackElements) { this.descriptions.fallbackElements = {}; } //check if html structure for element description is ready if (this.descriptions.fallbackElements[name]) { //if current element description is not the same as inner if (this.descriptions.fallbackElements[name].innerHTML !== inner) { //update element description this.descriptions.fallbackElements[name].innerHTML = inner; } } else { //create fallback html structure this._describeElementHTML('fallback', name, inner); } //if display is LABEL if (display === this.LABEL) { if (!this.descriptions.labelElements) { this.descriptions.labelElements = {}; } //if html structure for label element description is ready if (this.descriptions.labelElements[name]) { //if label element description is different if (this.descriptions.labelElements[name].innerHTML !== inner) { //update label element description this.descriptions.labelElements[name].innerHTML = inner; } } else { //create label element html structure this._describeElementHTML('label', name, inner); } } }; /* * * Helper functions for describe() and describeElement(). * */ // check that text is not LABEL or FALLBACK and ensure text ends with punctuation mark function _descriptionText(text) { if (text === 'label' || text === 'fallback') { throw new Error('description should not be LABEL or FALLBACK'); } //if string does not end with '.' if ( !text.endsWith('.') && !text.endsWith(';') && !text.endsWith(',') && !text.endsWith('?') && !text.endsWith('!') ) { //add '.' to the end of string text = text + '.'; } return text; } /* * Helper functions for describe() */ //creates HTML structure for canvas descriptions fn._describeHTML = function(type, text) { const cnvId = this.canvas.id; if (type === 'fallback') { //if there is no description container if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) { //if there are no accessible outputs (see textOutput() and gridOutput()) let html = `

`; if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) { //create description container +

for fallback description this.dummyDOM.querySelector(`#${cnvId}`).innerHTML = html; } else { //create description container +

for fallback description before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutput`) .insertAdjacentHTML('beforebegin', html); } } else { //if describeElement() has already created the container and added a table of elements //create fallback description

before the table this.dummyDOM .querySelector('#' + cnvId + fallbackTableId) .insertAdjacentHTML( 'beforebegin', `

` ); } //if the container for the description exists this.descriptions.fallback = this.dummyDOM.querySelector( `#${cnvId}${fallbackDescId}` ); this.descriptions.fallback.innerHTML = text; return; } else if (type === 'label') { //if there is no label container if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) { let html = `

`; //if there are no accessible outputs (see textOutput() and gridOutput()) if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) { //create label container +

for label description this.dummyDOM .querySelector('#' + cnvId) .insertAdjacentHTML('afterend', html); } else { //create label container +

for label description before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutputLabel`) .insertAdjacentHTML('beforebegin', html); } } else if (this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) { //if describeElement() has already created the container and added a table of elements //create label description

before the table this.dummyDOM .querySelector(`#${cnvId + labelTableId}`) .insertAdjacentHTML( 'beforebegin', `

` ); } this.descriptions.label = this.dummyDOM.querySelector( '#' + cnvId + labelDescId ); this.descriptions.label.innerHTML = text; return; } }; /* * Helper functions for describeElement(). */ //check that name is not LABEL or FALLBACK and ensure text ends with colon function _elementName(name) { if (name === 'label' || name === 'fallback') { throw new Error('element name should not be LABEL or FALLBACK'); } //check if last character of string n is '.', ';', or ',' if (name.endsWith('.') || name.endsWith(';') || name.endsWith(',')) { //replace last character with ':' name = name.replace(/.$/, ':'); } else if (!name.endsWith(':')) { //if string n does not end with ':' //add ':'' at the end of string name = name + ':'; } return name; } //creates HTML structure for element descriptions fn._describeElementHTML = function(type, name, text) { const cnvId = this.canvas.id; if (type === 'fallback') { //if there is no description container if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) { //if there are no accessible outputs (see textOutput() and gridOutput()) let html = `
Canvas elements and their descriptions
`; if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) { //create container + table for element descriptions this.dummyDOM.querySelector('#' + cnvId).innerHTML = html; } else { //create container + table for element descriptions before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutput`) .insertAdjacentHTML('beforebegin', html); } } else if (!this.dummyDOM.querySelector('#' + cnvId + fallbackTableId)) { //if describe() has already created the container and added a description //and there is no table create fallback table for element description after //fallback description this.dummyDOM .querySelector('#' + cnvId + fallbackDescId) .insertAdjacentHTML( 'afterend', `
Canvas elements and their descriptions
` ); } //create a table row for the element let tableRow = document.createElement('tr'); tableRow.id = cnvId + fallbackTableElId + name; this.dummyDOM .querySelector('#' + cnvId + fallbackTableId) .appendChild(tableRow); //update element description this.descriptions.fallbackElements[name] = this.dummyDOM.querySelector( `#${cnvId}${fallbackTableElId}${name}` ); this.descriptions.fallbackElements[name].innerHTML = text; return; } else if (type === 'label') { //If display is LABEL creates a div adjacent to the canvas element with //a table, a row header cell with the name of the elements, //and adds the description of the element in adjacent cell. //if there is no label description container if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) { //if there are no accessible outputs (see textOutput() and gridOutput()) let html = `
`; if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) { //create container + table for element descriptions this.dummyDOM .querySelector('#' + cnvId) .insertAdjacentHTML('afterend', html); } else { //create container + table for element descriptions before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutputLabel`) .insertAdjacentHTML('beforebegin', html); } } else if (!this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) { //if describe() has already created the label container and added a description //and there is no table create label table for element description after //label description this.dummyDOM .querySelector('#' + cnvId + labelDescId) .insertAdjacentHTML( 'afterend', `
` ); } //create a table row for the element label description let tableRow = document.createElement('tr'); tableRow.id = cnvId + labelTableElId + name; this.dummyDOM .querySelector('#' + cnvId + labelTableId) .appendChild(tableRow); //update element label description this.descriptions.labelElements[name] = this.dummyDOM.querySelector( `#${cnvId}${labelTableElId}${name}` ); this.descriptions.labelElements[name].innerHTML = text; } }; } if(typeof p5 !== 'undefined'){ describe(p5, p5.prototype); } /** * @module Environment * @submodule Environment * @for p5 * @requires core */ function gridOutput(p5, fn){ //the functions in this file support updating the grid output //updates gridOutput fn._updateGridOutput = function(idT) { if (this._renderer && this._renderer.isP3D) { if (!this._didOutputGridWebGLMessage) { this._didOutputGridWebGLMessage = true; console.error('gridOutput() does not yet work in WebGL mode.'); } return; } //if html structure is not there yet if (!this.dummyDOM.querySelector(`#${idT}_summary`)) { return; } let current = this._accessibleOutputs[idT]; //create shape details list let innerShapeDetails = _gridShapeDetails(idT, this.ingredients.shapes); //create summary let innerSummary = _gridSummary( innerShapeDetails.numShapes, this.ingredients.colors.background, this.width, this.height ); //create grid map let innerMap = _gridMap(idT, this.ingredients.shapes); //if it is different from current summary if (innerSummary !== current.summary.innerHTML) { //update current.summary.innerHTML = innerSummary; } //if it is different from current map if (innerMap !== current.map.innerHTML) { //update current.map.innerHTML = innerMap; } //if it is different from current shape details if (innerShapeDetails.details !== current.shapeDetails.innerHTML) { //update current.shapeDetails.innerHTML = innerShapeDetails.details; } this._accessibleOutputs[idT] = current; }; //creates spatial grid that maps the location of shapes function _gridMap(idT, ingredients) { let shapeNumber = 0; let table = ''; //create an array of arrays 10*10 of empty cells let cells = Array.from(Array(10), () => Array(10)); for (let x in ingredients) { for (let y in ingredients[x]) { let fill; if (x !== 'line') { fill = `${ ingredients[x][y].color } ${x}`; } else { fill = `${ ingredients[x][y].color } ${x} midpoint`; } // Check if shape is in canvas, skip if not if( ingredients[x][y].loc.locY < cells.length && ingredients[x][y].loc.locX < cells[ingredients[x][y].loc.locY].length ){ //if empty cell of location of shape is undefined if (!cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX]) { //fill it with shape info cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] = fill; //if a shape is already in that location } else { //add it cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] = cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] + ' ' + fill; } shapeNumber++; } } } //make table based on array for (let _r in cells) { let row = ''; for (let c in cells[_r]) { row = row + ''; if (cells[_r][c] !== undefined) { row = row + cells[_r][c]; } row = row + ''; } table = table + row + ''; } return table; } //creates grid summary function _gridSummary(numShapes, background, width, height) { let text = `${background} canvas, ${width} by ${height} pixels, contains ${ numShapes[0] }`; if (numShapes[0] === 1) { text = `${text} shape: ${numShapes[1]}`; } else { text = `${text} shapes: ${numShapes[1]}`; } return text; } //creates list of shapes function _gridShapeDetails(idT, ingredients) { let shapeDetails = ''; let shapes = ''; let totalShapes = 0; //goes trhough every shape type in ingredients for (let x in ingredients) { let shapeNum = 0; for (let y in ingredients[x]) { //it creates a line in a list let line = `
  • ${ ingredients[x][y].color } ${x},`; if (x === 'line') { line = line + ` location = ${ingredients[x][y].pos}, length = ${ ingredients[x][y].length } pixels`; } else { line = line + ` location = ${ingredients[x][y].pos}`; if (x !== 'point') { line = line + `, area = ${ingredients[x][y].area} %`; } line = line + '
  • '; } shapeDetails = shapeDetails + line; shapeNum++; totalShapes++; } if (shapeNum > 1) { shapes = `${shapes} ${shapeNum} ${x}s`; } else { shapes = `${shapes} ${shapeNum} ${x}`; } } return { numShapes: [totalShapes, shapes], details: shapeDetails }; } } if(typeof p5 !== 'undefined'){ gridOutput(p5, p5.prototype); } /** * @module Environment * @submodule Environment * @for p5 * @requires core */ function textOutput(p5, fn){ //the functions in this file support updating the text output //updates textOutput fn._updateTextOutput = function(idT) { if (this._renderer && this._renderer.isP3D) { if (!this._didOutputTextWebGLMessage) { this._didOutputTextWebGLMessage = true; console.error('textOutput() does not yet work in WebGL mode.'); } return; } //if html structure is not there yet if (!this.dummyDOM.querySelector(`#${idT}_summary`)) { return; } let current = this._accessibleOutputs[idT]; //create shape list let innerList = _shapeList(idT, this.ingredients.shapes); //create output summary let innerSummary = _textSummary( innerList.numShapes, this.ingredients.colors.background, this.width, this.height ); //create shape details let innerShapeDetails = _shapeDetails(idT, this.ingredients.shapes); //if it is different from current summary if (innerSummary !== current.summary.innerHTML) { //update current.summary.innerHTML = innerSummary; } //if it is different from current shape list if (innerList.listShapes !== current.list.innerHTML) { //update current.list.innerHTML = innerList.listShapes; } //if it is different from current shape details if (innerShapeDetails !== current.shapeDetails.innerHTML) { //update current.shapeDetails.innerHTML = innerShapeDetails; } this._accessibleOutputs[idT] = current; }; //Builds textOutput summary function _textSummary(numShapes, background, width, height) { let text = `Your output is a, ${width} by ${height} pixels, ${background} canvas containing the following`; if (numShapes === 1) { text = `${text} shape:`; } else { text = `${text} ${numShapes} shapes:`; } return text; } //Builds textOutput table with shape details function _shapeDetails(idT, ingredients) { let shapeDetails = ''; let shapeNumber = 0; //goes trhough every shape type in ingredients for (let x in ingredients) { //and for every shape for (let y in ingredients[x]) { //it creates a table row let row = `${ ingredients[x][y].color } ${x}`; if (x === 'line') { row = row + `location = ${ingredients[x][y].pos}length = ${ ingredients[x][y].length } pixels`; } else { row = row + `location = ${ingredients[x][y].pos}`; if (x !== 'point') { row = row + ` area = ${ingredients[x][y].area}%`; } row = row + ''; } shapeDetails = shapeDetails + row; shapeNumber++; } } return shapeDetails; } //Builds textOutput shape list function _shapeList(idT, ingredients) { let shapeList = ''; let shapeNumber = 0; //goes trhough every shape type in ingredients for (let x in ingredients) { for (let y in ingredients[x]) { //it creates a line in a list let _line = `
  • ${ ingredients[x][y].color } ${x}`; if (x === 'line') { _line = _line + `, ${ingredients[x][y].pos}, ${ ingredients[x][y].length } pixels long.
  • `; } else { _line = _line + `, at ${ingredients[x][y].pos}`; if (x !== 'point') { _line = _line + `, covering ${ingredients[x][y].area}% of the canvas`; } _line = _line + '.'; } shapeList = shapeList + _line; shapeNumber++; } } return { numShapes: shapeNumber, listShapes: shapeList }; } } if(typeof p5 !== 'undefined'){ textOutput(p5, p5.prototype); } /** * @module Environment * @submodule Environment * @for p5 * @requires core */ function outputs(p5, fn){ /** * Creates a screen reader-accessible description of shapes on the canvas. * * `textOutput()` adds a general description, list of shapes, and * table of shapes to the web page. The general description includes the * canvas size, canvas color, and number of shapes. For example, * `Your output is a, 100 by 100 pixels, gray canvas containing the following 2 shapes:`. * * A list of shapes follows the general description. The list describes the * color, location, and area of each shape. For example, * `a red circle at middle covering 3% of the canvas`. Each shape can be * selected to get more details. * * `textOutput()` uses its table of shapes as a list. The table describes the * shape, color, location, coordinates and area. For example, * `red circle location = middle area = 3%`. This is different from * gridOutput(), which uses its table as a grid. * * The `display` parameter is optional. It determines how the description is * displayed. If `LABEL` is passed, as in `textOutput(LABEL)`, the description * will be visible in a div element next to the canvas. Using `LABEL` creates * unhelpful duplicates for screen readers. Only use `LABEL` during * development. If `FALLBACK` is passed, as in `textOutput(FALLBACK)`, the * description will only be visible to screen readers. This is the default * mode. * * Read * Writing accessible canvas descriptions * to learn more about making sketches accessible. * * @method textOutput * @param {(FALLBACK|LABEL)} [display] either FALLBACK or LABEL. * * @example * function setup() { * // Add the text description. * textOutput(); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * * @example * function setup() { * // Add the text description and * // display it for debugging. * textOutput(LABEL); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * * @example * function setup(){ * createCanvas(100, 100); * } * * function draw() { * // Add the text description. * textOutput(); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } * * @example * function setup(){ * createCanvas(100, 100); * } * * function draw() { * // Add the text description and * // display it for debugging. * textOutput(LABEL); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } */ fn.textOutput = function(display) { // p5._validateParameters('textOutput', arguments); //if textOutput is already true if (this._accessibleOutputs.text) { return; } else { //make textOutput true this._accessibleOutputs.text = true; //create output for fallback this._createOutput('textOutput', 'Fallback'); if (display === this.LABEL) { //make textOutput label true this._accessibleOutputs.textLabel = true; //create output for label this._createOutput('textOutput', 'Label'); } } }; /** * Creates a screen reader-accessible description of shapes on the canvas. * * `gridOutput()` adds a general description, table of shapes, and list of * shapes to the web page. The general description includes the canvas size, * canvas color, and number of shapes. For example, * `gray canvas, 100 by 100 pixels, contains 2 shapes: 1 circle 1 square`. * * `gridOutput()` uses its table of shapes as a grid. Each shape in the grid * is placed in a cell whose row and column correspond to the shape's location * on the canvas. The grid cells describe the color and type of shape at that * location. For example, `red circle`. These descriptions can be selected * individually to get more details. This is different from * textOutput(), which uses its table as a list. * * A list of shapes follows the table. The list describes the color, type, * location, and area of each shape. For example, * `red circle, location = middle, area = 3 %`. * * The `display` parameter is optional. It determines how the description is * displayed. If `LABEL` is passed, as in `gridOutput(LABEL)`, the description * will be visible in a div element next to the canvas. Using `LABEL` creates * unhelpful duplicates for screen readers. Only use `LABEL` during * development. If `FALLBACK` is passed, as in `gridOutput(FALLBACK)`, the * description will only be visible to screen readers. This is the default * mode. * * Read * Writing accessible canvas descriptions * to learn more about making sketches accessible. * * @method gridOutput * @param {(FALLBACK|LABEL)} [display] either FALLBACK or LABEL. * * @example * function setup() { * // Add the grid description. * gridOutput(); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * * @example * function setup() { * // Add the grid description and * // display it for debugging. * gridOutput(LABEL); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * * @example * function setup() { * createCanvas(100, 100); * } * * function draw() { * // Add the grid description. * gridOutput(); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } * * @example * function setup(){ * createCanvas(100, 100); * } * * function draw() { * // Add the grid description and * // display it for debugging. * gridOutput(LABEL); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } */ fn.gridOutput = function(display) { // p5._validateParameters('gridOutput', arguments); //if gridOutput is already true if (this._accessibleOutputs.grid) { return; } else { //make gridOutput true this._accessibleOutputs.grid = true; //create output for fallback this._createOutput('gridOutput', 'Fallback'); if (display === this.LABEL) { //make gridOutput label true this._accessibleOutputs.gridLabel = true; //create output for label this._createOutput('gridOutput', 'Label'); } } }; //helper function returns true when accessible outputs are true fn._addAccsOutput = function() { //if there are no accessible outputs create object with all false if (!this._accessibleOutputs) { this._accessibleOutputs = { text: false, grid: false, textLabel: false, gridLabel: false }; } return this._accessibleOutputs.grid || this._accessibleOutputs.text; }; //helper function that creates html structure for accessible outputs fn._createOutput = function(type, display) { let cnvId = this.canvas.id; //if there are no ingredients create object. this object stores data for the outputs if (!this.ingredients) { this.ingredients = { shapes: {}, colors: { background: 'white', fill: 'white', stroke: 'black' }, pShapes: '', pBackground: '' }; } //if there is no dummyDOM create it if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; } let cIdT, container, inner; let query = ''; if (display === 'Fallback') { cIdT = cnvId + type; container = cnvId + 'accessibleOutput'; if (!this.dummyDOM.querySelector(`#${container}`)) { //if there is no canvas description (see describe() and describeElement()) if (!this.dummyDOM.querySelector(`#${cnvId}_Description`)) { //create html structure inside of canvas this.dummyDOM.querySelector( `#${cnvId}` ).innerHTML = `
    `; } else { //create html structure after canvas description container this.dummyDOM .querySelector(`#${cnvId}_Description`) .insertAdjacentHTML( 'afterend', `
    ` ); } } } else if (display === 'Label') { query = display; cIdT = cnvId + type + display; container = cnvId + 'accessibleOutput' + display; if (!this.dummyDOM.querySelector(`#${container}`)) { //if there is no canvas description label (see describe() and describeElement()) if (!this.dummyDOM.querySelector(`#${cnvId}_Label`)) { //create html structure adjacent to canvas this.dummyDOM .querySelector(`#${cnvId}`) .insertAdjacentHTML('afterend', `
    `); } else { //create html structure after canvas label this.dummyDOM .querySelector(`#${cnvId}_Label`) .insertAdjacentHTML('afterend', `
    `); } } } //create an object to store the latest output. this object is used in _updateTextOutput() and _updateGridOutput() this._accessibleOutputs[cIdT] = {}; if (type === 'textOutput') { query = `#${cnvId}gridOutput${query}`; //query is used to check if gridOutput already exists inner = `
    Text Output

      `; //if gridOutput already exists if (this.dummyDOM.querySelector(query)) { //create textOutput before gridOutput this.dummyDOM .querySelector(query) .insertAdjacentHTML('beforebegin', inner); } else { //create output inside of container this.dummyDOM.querySelector(`#${container}`).innerHTML = inner; } //store output html elements this._accessibleOutputs[cIdT].list = this.dummyDOM.querySelector( `#${cIdT}_list` ); } else if (type === 'gridOutput') { query = `#${cnvId}textOutput${query}`; //query is used to check if textOutput already exists inner = `
      Grid Output

        `; //if textOutput already exists if (this.dummyDOM.querySelector(query)) { //create gridOutput after textOutput this.dummyDOM.querySelector(query).insertAdjacentHTML('afterend', inner); } else { //create output inside of container this.dummyDOM.querySelector(`#${container}`).innerHTML = inner; } //store output html elements this._accessibleOutputs[cIdT].map = this.dummyDOM.querySelector( `#${cIdT}_map` ); } this._accessibleOutputs[cIdT].shapeDetails = this.dummyDOM.querySelector( `#${cIdT}_shapeDetails` ); this._accessibleOutputs[cIdT].summary = this.dummyDOM.querySelector( `#${cIdT}_summary` ); }; //this function is called at the end of setup and draw if using //accessibleOutputs and calls update functions of outputs fn._updateAccsOutput = function() { let cnvId = this.canvas.id; //if the shapes are not the same as before if ( JSON.stringify(this.ingredients.shapes) !== this.ingredients.pShapes || this.ingredients.colors.background !== this.ingredients.pBackground ) { //save current shapes as string in pShapes this.ingredients.pShapes = JSON.stringify(this.ingredients.shapes); if (this._accessibleOutputs.text) { this._updateTextOutput(cnvId + 'textOutput'); } if (this._accessibleOutputs.grid) { this._updateGridOutput(cnvId + 'gridOutput'); } if (this._accessibleOutputs.textLabel) { this._updateTextOutput(cnvId + 'textOutputLabel'); } if (this._accessibleOutputs.gridLabel) { this._updateGridOutput(cnvId + 'gridOutputLabel'); } } }; //helper function that resets all ingredients when background is called //and saves background color name fn._accsBackground = function(args) { //save current shapes as string in pShapes this.ingredients.pShapes = JSON.stringify(this.ingredients.shapes); this.ingredients.pBackground = this.ingredients.colors.background; //empty shapes JSON this.ingredients.shapes = {}; //update background different if (this.ingredients.colors.backgroundRGBA !== args) { this.ingredients.colors.backgroundRGBA = args; this.ingredients.colors.background = this._rgbColorName(args); } }; //helper function that gets fill and stroke of shapes fn._accsCanvasColors = function(f, args) { if (f === 'fill') { //update fill different if (this.ingredients.colors.fillRGBA !== args) { this.ingredients.colors.fillRGBA = args; this.ingredients.colors.fill = this._rgbColorName(args); } } else if (f === 'stroke') { //update stroke if different if (this.ingredients.colors.strokeRGBA !== args) { this.ingredients.colors.strokeRGBA = args; this.ingredients.colors.stroke = this._rgbColorName(args); } } }; //builds ingredients.shapes used for building outputs fn._accsOutput = function(f, args) { if (f === 'ellipse' && args[2] === args[3]) { f = 'circle'; } else if (f === 'rectangle' && args[2] === args[3]) { f = 'square'; } let include = {}; let add = true; let middle = _getMiddle(f, args); if (f === 'line') { //make color stroke include.color = this.ingredients.colors.stroke; //get lenght include.length = Math.round( Math.hypot(args[2] - args[0], args[3] - args[1]) ); //get position of end points let p1 = this._getPos(args[0], [1]); let p2 = this._getPos(args[2], [3]); include.loc = _canvasLocator(middle, this.width, this.height); if (p1 === p2) { include.pos = `at ${p1}`; } else { include.pos = `from ${p1} to ${p2}`; } } else { if (f === 'point') { //make color stroke include.color = this.ingredients.colors.stroke; } else { //make color fill include.color = this.ingredients.colors.fill; //get area of shape include.area = this._getArea(f, args); } //get middle of shapes //calculate position using middle of shape include.pos = this._getPos(...middle); //calculate location using middle of shape include.loc = _canvasLocator(middle, this.width, this.height); } //if it is the first time this shape is created if (!this.ingredients.shapes[f]) { this.ingredients.shapes[f] = [include]; //if other shapes of this type have been created } else { //for every shape of this type for (let y in this.ingredients.shapes[f]) { //compare it with current shape and if it already exists make add false if ( JSON.stringify(this.ingredients.shapes[f][y]) === JSON.stringify(include) ) { add = false; } } //add shape by pushing it to the end if (add === true) { this.ingredients.shapes[f].push(include); } } }; //gets middle point / centroid of shape function _getMiddle(f, args) { let x, y; if ( f === 'rectangle' || f === 'ellipse' || f === 'arc' || f === 'circle' || f === 'square' ) { x = Math.round(args[0] + args[2] / 2); y = Math.round(args[1] + args[3] / 2); } else if (f === 'triangle') { x = (args[0] + args[2] + args[4]) / 3; y = (args[1] + args[3] + args[5]) / 3; } else if (f === 'quadrilateral') { x = (args[0] + args[2] + args[4] + args[6]) / 4; y = (args[1] + args[3] + args[5] + args[7]) / 4; } else if (f === 'line') { x = (args[0] + args[2]) / 2; y = (args[1] + args[3]) / 2; } else { x = args[0]; y = args[1]; } return [x, y]; } //gets position of shape in the canvas fn._getPos = function (x, y) { const { x: transformedX, y: transformedY } = this.worldToScreen(new p5.Vector(x, y)); const canvasWidth = this.width; const canvasHeight = this.height; if (transformedX < 0.4 * canvasWidth) { if (transformedY < 0.4 * canvasHeight) { return 'top left'; } else if (transformedY > 0.6 * canvasHeight) { return 'bottom left'; } else { return 'mid left'; } } else if (transformedX > 0.6 * canvasWidth) { if (transformedY < 0.4 * canvasHeight) { return 'top right'; } else if (transformedY > 0.6 * canvasHeight) { return 'bottom right'; } else { return 'mid right'; } } else { if (transformedY < 0.4 * canvasHeight) { return 'top middle'; } else if (transformedY > 0.6 * canvasHeight) { return 'bottom middle'; } else { return 'middle'; } } }; //locates shape in a 10*10 grid function _canvasLocator(args, canvasWidth, canvasHeight) { const noRows = 10; const noCols = 10; let locX = Math.floor(args[0] / canvasWidth * noRows); let locY = Math.floor(args[1] / canvasHeight * noCols); if (locX === noRows) { locX = locX - 1; } if (locY === noCols) { locY = locY - 1; } return { locX, locY }; } //calculates area of shape fn._getArea = function (objectType, shapeArgs) { let objectArea = 0; if (objectType === 'arc') { // area of full ellipse = PI * horizontal radius * vertical radius. // therefore, area of arc = difference bet. arc's start and end radians * horizontal radius * vertical radius. // the below expression is adjusted for negative values and differences in arc's start and end radians over PI*2 const arcSizeInRadians = ((shapeArgs[5] - shapeArgs[4]) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2); objectArea = arcSizeInRadians * shapeArgs[2] * shapeArgs[3] / 8; if (shapeArgs[6] === 'open' || shapeArgs[6] === 'chord') { // when the arc's mode is OPEN or CHORD, we need to account for the area of the triangle that is formed to close the arc // (Ax( By − Cy) + Bx(Cy − Ay) + Cx(Ay − By ) )/2 const Ax = shapeArgs[0]; const Ay = shapeArgs[1]; const Bx = shapeArgs[0] + shapeArgs[2] / 2 * Math.cos(shapeArgs[4]).toFixed(2); const By = shapeArgs[1] + shapeArgs[3] / 2 * Math.sin(shapeArgs[4]).toFixed(2); const Cx = shapeArgs[0] + shapeArgs[2] / 2 * Math.cos(shapeArgs[5]).toFixed(2); const Cy = shapeArgs[1] + shapeArgs[3] / 2 * Math.sin(shapeArgs[5]).toFixed(2); const areaOfExtraTriangle = Math.abs(Ax * (By - Cy) + Bx * (Cy - Ay) + Cx * (Ay - By)) / 2; if (arcSizeInRadians > Math.PI) { objectArea = objectArea + areaOfExtraTriangle; } else { objectArea = objectArea - areaOfExtraTriangle; } } } else if (objectType === 'ellipse' || objectType === 'circle') { objectArea = 3.14 * shapeArgs[2] / 2 * shapeArgs[3] / 2; } else if (objectType === 'line') { objectArea = 0; } else if (objectType === 'point') { objectArea = 0; } else if (objectType === 'quadrilateral') { // ((x4+x1)*(y4-y1)+(x1+x2)*(y1-y2)+(x2+x3)*(y2-y3)+(x3+x4)*(y3-y4))/2 objectArea = Math.abs( (shapeArgs[6] + shapeArgs[0]) * (shapeArgs[7] - shapeArgs[1]) + (shapeArgs[0] + shapeArgs[2]) * (shapeArgs[1] - shapeArgs[3]) + (shapeArgs[2] + shapeArgs[4]) * (shapeArgs[3] - shapeArgs[5]) + (shapeArgs[4] + shapeArgs[6]) * (shapeArgs[5] - shapeArgs[7]) ) / 2; } else if (objectType === 'rectangle' || objectType === 'square') { objectArea = shapeArgs[2] * shapeArgs[3]; } else if (objectType === 'triangle') { objectArea = Math.abs( shapeArgs[0] * (shapeArgs[3] - shapeArgs[5]) + shapeArgs[2] * (shapeArgs[5] - shapeArgs[1]) + shapeArgs[4] * (shapeArgs[1] - shapeArgs[3]) ) / 2; // (Ax( By − Cy) + Bx(Cy − Ay) + Cx(Ay − By ))/2 } // Store the positions of the canvas corners const canvasWidth = this.width * this._renderer._pixelDensity; const canvasHeight = this.height * this._renderer._pixelDensity; const canvasCorners = [ new DOMPoint(0, 0), new DOMPoint(canvasWidth, 0), new DOMPoint(canvasWidth, canvasHeight), new DOMPoint(0, canvasHeight) ]; // Apply the inverse of the current transformations to the canvas corners const currentTransform = this._renderer.isP3D ? new DOMMatrix(this._renderer.uMVMatrix.mat4) : this.drawingContext.getTransform(); const invertedTransform = currentTransform.inverse(); const tc = canvasCorners.map( corner => corner.matrixTransform(invertedTransform) ); /* Use same shoelace formula used for quad area (above) to calculate the area of the canvas with inverted transformation applied */ const transformedCanvasArea = Math.abs( (tc[3].x + tc[0].x) * (tc[3].y - tc[0].y) + (tc[0].x + tc[1].x) * (tc[0].y - tc[1].y) + (tc[1].x + tc[2].x) * (tc[1].y - tc[2].y)+ (tc[2].x + tc[3].x) * (tc[2].y - tc[3].y) ) / 2; /* Compare area of shape (minus transformations) to area of canvas with inverted transformation applied. Return percentage */ const untransformedArea = Math.round( objectArea * 100 / (transformedCanvasArea) ); return untransformedArea; }; } if(typeof p5 !== 'undefined'){ outputs(p5, p5.prototype); } /** * @module Color * @submodule Color Conversion * @for p5 * @requires core */ p5$2.ColorConversion = { /** * Convert an HSBA array to HSLA. */ _hsbaToHSLA(hsba) { const hue = hsba[0]; let sat = hsba[1]; const val = hsba[2]; // Calculate lightness. const li = (2 - sat) * val / 2; // Convert saturation. if (li !== 0) { if (li === 1) { sat = 0; } else if (li < 0.5) { sat = sat / (2 - sat); } else { sat = sat * val / (2 - li * 2); } } // Hue and alpha stay the same. return [hue, sat, li, hsba[3]]; }, /** * Convert an HSBA array to RGBA. */ _hsbaToRGBA(hsba) { const hue = hsba[0] * 6; // We will split hue into 6 sectors. const sat = hsba[1]; const val = hsba[2]; let RGBA = []; if (sat === 0) { RGBA = [val, val, val, hsba[3]]; // Return early if grayscale. } else { const sector = Math.floor(hue); const tint1 = val * (1 - sat); const tint2 = val * (1 - sat * (hue - sector)); const tint3 = val * (1 - sat * (1 + sector - hue)); let red, green, blue; if (sector === 1) { // Yellow to green. red = tint2; green = val; blue = tint1; } else if (sector === 2) { // Green to cyan. red = tint1; green = val; blue = tint3; } else if (sector === 3) { // Cyan to blue. red = tint1; green = tint2; blue = val; } else if (sector === 4) { // Blue to magenta. red = tint3; green = tint1; blue = val; } else if (sector === 5) { // Magenta to red. red = val; green = tint1; blue = tint2; } else { // Red to yellow (sector could be 0 or 6). red = val; green = tint3; blue = tint1; } RGBA = [red, green, blue, hsba[3]]; } return RGBA; }, /** * Convert an HSLA array to HSBA. */ _hslaToHSBA(hsla) { const hue = hsla[0]; let sat = hsla[1]; const li = hsla[2]; // Calculate brightness. let val; if (li < 0.5) { val = (1 + sat) * li; } else { val = li + sat - li * sat; } // Convert saturation. sat = 2 * (val - li) / val; // Hue and alpha stay the same. return [hue, sat, val, hsla[3]]; }, /** * Convert an HSLA array to RGBA. * * We need to change basis from HSLA to something that can be more easily be * projected onto RGBA. We will choose hue and brightness as our first two * components, and pick a convenient third one ('zest') so that we don't need * to calculate formal HSBA saturation. */ _hslaToRGBA(hsla) { const hue = hsla[0] * 6; // We will split hue into 6 sectors. const sat = hsla[1]; const li = hsla[2]; let RGBA = []; if (sat === 0) { RGBA = [li, li, li, hsla[3]]; // Return early if grayscale. } else { // Calculate brightness. let val; if (li < 0.5) { val = (1 + sat) * li; } else { val = li + sat - li * sat; } // Define zest. const zest = 2 * li - val; // Implement projection (project onto green by default). const hzvToRGB = (hue, zest, val) => { if (hue < 0) { // Hue must wrap to allow projection onto red and blue. hue += 6; } else if (hue >= 6) { hue -= 6; } if (hue < 1) { // Red to yellow (increasing green). return zest + (val - zest) * hue; } else if (hue < 3) { // Yellow to cyan (greatest green). return val; } else if (hue < 4) { // Cyan to blue (decreasing green). return zest + (val - zest) * (4 - hue); } else { // Blue to red (least green). return zest; } }; // Perform projections, offsetting hue as necessary. RGBA = [ hzvToRGB(hue + 2, zest, val), hzvToRGB(hue, zest, val), hzvToRGB(hue - 2, zest, val), hsla[3] ]; } return RGBA; }, /** * Convert an RGBA array to HSBA. */ _rgbaToHSBA(rgba) { const red = rgba[0]; const green = rgba[1]; const blue = rgba[2]; const val = Math.max(red, green, blue); const chroma = val - Math.min(red, green, blue); let hue, sat; if (chroma === 0) { // Return early if grayscale. hue = 0; sat = 0; } else { sat = chroma / val; if (red === val) { // Magenta to yellow. hue = (green - blue) / chroma; } else if (green === val) { // Yellow to cyan. hue = 2 + (blue - red) / chroma; } else if (blue === val) { // Cyan to magenta. hue = 4 + (red - green) / chroma; } if (hue < 0) { // Confine hue to the interval [0, 1). hue += 6; } else if (hue >= 6) { hue -= 6; } } return [hue / 6, sat, val, rgba[3]]; }, /** * Convert an RGBA array to HSLA. */ _rgbaToHSLA(rgba) { const red = rgba[0]; const green = rgba[1]; const blue = rgba[2]; const val = Math.max(red, green, blue); const min = Math.min(red, green, blue); const li = val + min; // We will halve this later. const chroma = val - min; let hue, sat; if (chroma === 0) { // Return early if grayscale. hue = 0; sat = 0; } else { if (li < 1) { sat = chroma / li; } else { sat = chroma / (2 - li); } if (red === val) { // Magenta to yellow. hue = (green - blue) / chroma; } else if (green === val) { // Yellow to cyan. hue = 2 + (blue - red) / chroma; } else if (blue === val) { // Cyan to magenta. hue = 4 + (red - green) / chroma; } if (hue < 0) { // Confine hue to the interval [0, 1). hue += 6; } else if (hue >= 6) { hue -= 6; } } return [hue / 6, sat, li / 2, rgba[3]]; } }; var color_conversion = p5$2.ColorConversion; /** * @module Environment * @submodule Environment * @for p5 * @requires core */ function colorNamer(p5, fn){ //stores the original hsb values let originalHSB; //stores values for color name exceptions const colorExceptions = [ { h: 0, s: 0, b: 0.8275, name: 'gray' }, { h: 0, s: 0, b: 0.8627, name: 'gray' }, { h: 0, s: 0, b: 0.7529, name: 'gray' }, { h: 0.0167, s: 0.1176, b: 1, name: 'light pink' } ]; //stores values for color names const colorLookUp = [ { h: 0, s: 0, b: 0, name: 'black' }, { h: 0, s: 0, b: 0.5, name: 'gray' }, { h: 0, s: 0, b: 1, name: 'white' }, { h: 0, s: 0.5, b: 0.5, name: 'dark maroon' }, { h: 0, s: 0.5, b: 1, name: 'salmon pink' }, { h: 0, s: 1, b: 0, name: 'black' }, { h: 0, s: 1, b: 0.5, name: 'dark red' }, { h: 0, s: 1, b: 1, name: 'red' }, { h: 5, s: 0, b: 1, name: 'very light peach' }, { h: 5, s: 0.5, b: 0.5, name: 'brown' }, { h: 5, s: 0.5, b: 1, name: 'peach' }, { h: 5, s: 1, b: 0.5, name: 'brick red' }, { h: 5, s: 1, b: 1, name: 'crimson' }, { h: 10, s: 0, b: 1, name: 'light peach' }, { h: 10, s: 0.5, b: 0.5, name: 'brown' }, { h: 10, s: 0.5, b: 1, name: 'light orange' }, { h: 10, s: 1, b: 0.5, name: 'brown' }, { h: 10, s: 1, b: 1, name: 'orange' }, { h: 15, s: 0, b: 1, name: 'very light yellow' }, { h: 15, s: 0.5, b: 0.5, name: 'olive green' }, { h: 15, s: 0.5, b: 1, name: 'light yellow' }, { h: 15, s: 1, b: 0, name: 'dark olive green' }, { h: 15, s: 1, b: 0.5, name: 'olive green' }, { h: 15, s: 1, b: 1, name: 'yellow' }, { h: 20, s: 0, b: 1, name: 'very light yellow' }, { h: 20, s: 0.5, b: 0.5, name: 'olive green' }, { h: 20, s: 0.5, b: 1, name: 'light yellow green' }, { h: 20, s: 1, b: 0, name: 'dark olive green' }, { h: 20, s: 1, b: 0.5, name: 'dark yellow green' }, { h: 20, s: 1, b: 1, name: 'yellow green' }, { h: 25, s: 0.5, b: 0.5, name: 'dark yellow green' }, { h: 25, s: 0.5, b: 1, name: 'light green' }, { h: 25, s: 1, b: 0.5, name: 'dark green' }, { h: 25, s: 1, b: 1, name: 'green' }, { h: 30, s: 0.5, b: 1, name: 'light green' }, { h: 30, s: 1, b: 0.5, name: 'dark green' }, { h: 30, s: 1, b: 1, name: 'green' }, { h: 35, s: 0, b: 0.5, name: 'light green' }, { h: 35, s: 0, b: 1, name: 'very light green' }, { h: 35, s: 0.5, b: 0.5, name: 'dark green' }, { h: 35, s: 0.5, b: 1, name: 'light green' }, { h: 35, s: 1, b: 0, name: 'very dark green' }, { h: 35, s: 1, b: 0.5, name: 'dark green' }, { h: 35, s: 1, b: 1, name: 'green' }, { h: 40, s: 0, b: 1, name: 'very light green' }, { h: 40, s: 0.5, b: 0.5, name: 'dark green' }, { h: 40, s: 0.5, b: 1, name: 'light green' }, { h: 40, s: 1, b: 0.5, name: 'dark green' }, { h: 40, s: 1, b: 1, name: 'green' }, { h: 45, s: 0.5, b: 1, name: 'light turquoise' }, { h: 45, s: 1, b: 0.5, name: 'dark turquoise' }, { h: 45, s: 1, b: 1, name: 'turquoise' }, { h: 50, s: 0, b: 1, name: 'light sky blue' }, { h: 50, s: 0.5, b: 0.5, name: 'dark cyan' }, { h: 50, s: 0.5, b: 1, name: 'light cyan' }, { h: 50, s: 1, b: 0.5, name: 'dark cyan' }, { h: 50, s: 1, b: 1, name: 'cyan' }, { h: 55, s: 0, b: 1, name: 'light sky blue' }, { h: 55, s: 0.5, b: 1, name: 'light sky blue' }, { h: 55, s: 1, b: 0.5, name: 'dark blue' }, { h: 55, s: 1, b: 1, name: 'sky blue' }, { h: 60, s: 0, b: 0.5, name: 'gray' }, { h: 60, s: 0, b: 1, name: 'very light blue' }, { h: 60, s: 0.5, b: 0.5, name: 'blue' }, { h: 60, s: 0.5, b: 1, name: 'light blue' }, { h: 60, s: 1, b: 0.5, name: 'navy blue' }, { h: 60, s: 1, b: 1, name: 'blue' }, { h: 65, s: 0, b: 1, name: 'lavender' }, { h: 65, s: 0.5, b: 0.5, name: 'navy blue' }, { h: 65, s: 0.5, b: 1, name: 'light purple' }, { h: 65, s: 1, b: 0.5, name: 'dark navy blue' }, { h: 65, s: 1, b: 1, name: 'blue' }, { h: 70, s: 0, b: 1, name: 'lavender' }, { h: 70, s: 0.5, b: 0.5, name: 'navy blue' }, { h: 70, s: 0.5, b: 1, name: 'lavender blue' }, { h: 70, s: 1, b: 0.5, name: 'dark navy blue' }, { h: 70, s: 1, b: 1, name: 'blue' }, { h: 75, s: 0.5, b: 1, name: 'lavender' }, { h: 75, s: 1, b: 0.5, name: 'dark purple' }, { h: 75, s: 1, b: 1, name: 'purple' }, { h: 80, s: 0.5, b: 1, name: 'pinkish purple' }, { h: 80, s: 1, b: 0.5, name: 'dark purple' }, { h: 80, s: 1, b: 1, name: 'purple' }, { h: 85, s: 0, b: 1, name: 'light pink' }, { h: 85, s: 0.5, b: 0.5, name: 'purple' }, { h: 85, s: 0.5, b: 1, name: 'light fuchsia' }, { h: 85, s: 1, b: 0.5, name: 'dark fuchsia' }, { h: 85, s: 1, b: 1, name: 'fuchsia' }, { h: 90, s: 0.5, b: 0.5, name: 'dark fuchsia' }, { h: 90, s: 0.5, b: 1, name: 'hot pink' }, { h: 90, s: 1, b: 0.5, name: 'dark fuchsia' }, { h: 90, s: 1, b: 1, name: 'fuchsia' }, { h: 95, s: 0, b: 1, name: 'pink' }, { h: 95, s: 0.5, b: 1, name: 'light pink' }, { h: 95, s: 1, b: 0.5, name: 'dark magenta' }, { h: 95, s: 1, b: 1, name: 'magenta' } ]; //returns text with color name function _calculateColor(hsb) { let colortext; //round hue if (hsb[0] !== 0) { hsb[0] = Math.round(hsb[0] * 100); let hue = hsb[0].toString().split(''); const last = hue.length - 1; hue[last] = parseInt(hue[last]); //if last digit of hue is < 2.5 make it 0 if (hue[last] < 2.5) { hue[last] = 0; //if last digit of hue is >= 2.5 and less than 7.5 make it 5 } else if (hue[last] >= 2.5 && hue[last] < 7.5) { hue[last] = 5; } //if hue only has two digits if (hue.length === 2) { hue[0] = parseInt(hue[0]); //if last is greater than 7.5 if (hue[last] >= 7.5) { //add one to the tens hue[last] = 0; hue[0] = hue[0] + 1; } hsb[0] = hue[0] * 10 + hue[1]; } else { if (hue[last] >= 7.5) { hsb[0] = 10; } else { hsb[0] = hue[last]; } } } //map brightness from 0 to 1 hsb[2] = hsb[2] / 255; //round saturation and brightness for (let i = hsb.length - 1; i >= 1; i--) { if (hsb[i] <= 0.25) { hsb[i] = 0; } else if (hsb[i] > 0.25 && hsb[i] < 0.75) { hsb[i] = 0.5; } else { hsb[i] = 1; } } //after rounding, if the values are hue 0, saturation 0 and brightness 1 //look at color exceptions which includes several tones from white to gray if (hsb[0] === 0 && hsb[1] === 0 && hsb[2] === 1) { //round original hsb values for (let i = 2; i >= 0; i--) { originalHSB[i] = Math.round(originalHSB[i] * 10000) / 10000; } //compare with the values in the colorExceptions array for (let e = 0; e < colorExceptions.length; e++) { if ( colorExceptions[e].h === originalHSB[0] && colorExceptions[e].s === originalHSB[1] && colorExceptions[e].b === originalHSB[2] ) { colortext = colorExceptions[e].name; break; } else { //if there is no match return white colortext = 'white'; } } } else { //otherwise, compare with values in colorLookUp for (let i = 0; i < colorLookUp.length; i++) { if ( colorLookUp[i].h === hsb[0] && colorLookUp[i].s === hsb[1] && colorLookUp[i].b === hsb[2] ) { colortext = colorLookUp[i].name; break; } } } return colortext; } //gets rgba and returs a color name fn._rgbColorName = function(arg) { //conversts rgba to hsb let hsb = color_conversion._rgbaToHSBA(arg); //stores hsb in global variable originalHSB = hsb; //calculate color name return _calculateColor([hsb[0], hsb[1], hsb[2]]); }; } if(typeof p5 !== 'undefined'){ colorNamer(p5, p5.prototype); } function accessibility(p5){ p5.registerAddon(describe); p5.registerAddon(gridOutput); p5.registerAddon(textOutput); p5.registerAddon(outputs); p5.registerAddon(colorNamer); } function color(p5){ p5.registerAddon(creatingReading); p5.registerAddon(color$1); p5.registerAddon(setting); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return (String )(t); } function toPropertyKey(t) { var i = toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _defineProperty(e, r, t) { return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: true, configurable: true, writable: true }) : e[r] = t, e; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? Object(arguments[r]) : {}, o = Object.keys(t); "function" == typeof Object.getOwnPropertySymbols && o.push.apply(o, Object.getOwnPropertySymbols(t).filter(function (e) { return Object.getOwnPropertyDescriptor(t, e).enumerable; })), o.forEach(function (r) { _defineProperty(e, r, t[r]); }); } return e; } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: false }), e; } function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; } function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); } function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); } function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: true, configurable: true } }), Object.defineProperty(t, "prototype", { writable: false }), e && _setPrototypeOf(t, e); } var consoleLogger = { type: 'logger', log: function log(args) { this.output('log', args); }, warn: function warn(args) { this.output('warn', args); }, error: function error(args) { this.output('error', args); }, output: function output(type, args) { if (console && console[type]) console[type].apply(console, args); } }; var Logger = function () { function Logger(concreteLogger) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, Logger); this.init(concreteLogger, options); } _createClass(Logger, [{ key: "init", value: function init(concreteLogger) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this.prefix = options.prefix || 'i18next:'; this.logger = concreteLogger || consoleLogger; this.options = options; this.debug = options.debug; } }, { key: "setDebug", value: function setDebug(bool) { this.debug = bool; } }, { key: "log", value: function log() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return this.forward(args, 'log', '', true); } }, { key: "warn", value: function warn() { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return this.forward(args, 'warn', '', true); } }, { key: "error", value: function error() { for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } return this.forward(args, 'error', ''); } }, { key: "deprecate", value: function deprecate() { for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { args[_key4] = arguments[_key4]; } return this.forward(args, 'warn', 'WARNING DEPRECATED: ', true); } }, { key: "forward", value: function forward(args, lvl, prefix, debugOnly) { if (debugOnly && !this.debug) return null; if (typeof args[0] === 'string') args[0] = "".concat(prefix).concat(this.prefix, " ").concat(args[0]); return this.logger[lvl](args); } }, { key: "create", value: function create(moduleName) { return new Logger(this.logger, _objectSpread({}, { prefix: "".concat(this.prefix, ":").concat(moduleName, ":") }, this.options)); } }]); return Logger; }(); var baseLogger = new Logger(); var EventEmitter = function () { function EventEmitter() { _classCallCheck(this, EventEmitter); this.observers = {}; } _createClass(EventEmitter, [{ key: "on", value: function on(events, listener) { var _this = this; events.split(' ').forEach(function (event) { _this.observers[event] = _this.observers[event] || []; _this.observers[event].push(listener); }); return this; } }, { key: "off", value: function off(event, listener) { if (!this.observers[event]) return; if (!listener) { delete this.observers[event]; return; } this.observers[event] = this.observers[event].filter(function (l) { return l !== listener; }); } }, { key: "emit", value: function emit(event) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } if (this.observers[event]) { var cloned = [].concat(this.observers[event]); cloned.forEach(function (observer) { observer.apply(void 0, args); }); } if (this.observers['*']) { var _cloned = [].concat(this.observers['*']); _cloned.forEach(function (observer) { observer.apply(observer, [event].concat(args)); }); } } }]); return EventEmitter; }(); function defer() { var res; var rej; var promise = new Promise(function (resolve, reject) { res = resolve; rej = reject; }); promise.resolve = res; promise.reject = rej; return promise; } function makeString(object) { if (object == null) return ''; return '' + object; } function copy(a, s, t) { a.forEach(function (m) { if (s[m]) t[m] = s[m]; }); } function getLastOfPath(object, path, Empty) { function cleanKey(key) { return key && key.indexOf('###') > -1 ? key.replace(/###/g, '.') : key; } function canNotTraverseDeeper() { return !object || typeof object === 'string'; } var stack = typeof path !== 'string' ? [].concat(path) : path.split('.'); while (stack.length > 1) { if (canNotTraverseDeeper()) return {}; var key = cleanKey(stack.shift()); if (!object[key] && Empty) object[key] = new Empty(); if (Object.prototype.hasOwnProperty.call(object, key)) { object = object[key]; } else { object = {}; } } if (canNotTraverseDeeper()) return {}; return { obj: object, k: cleanKey(stack.shift()) }; } function setPath(object, path, newValue) { var _getLastOfPath = getLastOfPath(object, path, Object), obj = _getLastOfPath.obj, k = _getLastOfPath.k; obj[k] = newValue; } function pushPath(object, path, newValue, concat) { var _getLastOfPath2 = getLastOfPath(object, path, Object), obj = _getLastOfPath2.obj, k = _getLastOfPath2.k; obj[k] = obj[k] || []; obj[k].push(newValue); } function getPath(object, path) { var _getLastOfPath3 = getLastOfPath(object, path), obj = _getLastOfPath3.obj, k = _getLastOfPath3.k; if (!obj) return undefined; return obj[k]; } function getPathWithDefaults(data, defaultData, key) { var value = getPath(data, key); if (value !== undefined) { return value; } return getPath(defaultData, key); } function deepExtend(target, source, overwrite) { for (var prop in source) { if (prop !== '__proto__' && prop !== 'constructor') { if (prop in target) { if (typeof target[prop] === 'string' || target[prop] instanceof String || typeof source[prop] === 'string' || source[prop] instanceof String) { if (overwrite) target[prop] = source[prop]; } else { deepExtend(target[prop], source[prop], overwrite); } } else { target[prop] = source[prop]; } } } return target; } function regexEscape(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } var _entityMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/' }; function escape(data) { if (typeof data === 'string') { return data.replace(/[&<>"'\/]/g, function (s) { return _entityMap[s]; }); } return data; } var isIE10 = typeof window !== 'undefined' && window.navigator && window.navigator.userAgent && window.navigator.userAgent.indexOf('MSIE') > -1; var ResourceStore = function (_EventEmitter) { _inherits(ResourceStore, _EventEmitter); function ResourceStore(data) { var _this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { ns: ['translation'], defaultNS: 'translation' }; _classCallCheck(this, ResourceStore); _this = _possibleConstructorReturn(this, _getPrototypeOf(ResourceStore).call(this)); if (isIE10) { EventEmitter.call(_assertThisInitialized(_this)); } _this.data = data || {}; _this.options = options; if (_this.options.keySeparator === undefined) { _this.options.keySeparator = '.'; } return _this; } _createClass(ResourceStore, [{ key: "addNamespaces", value: function addNamespaces(ns) { if (this.options.ns.indexOf(ns) < 0) { this.options.ns.push(ns); } } }, { key: "removeNamespaces", value: function removeNamespaces(ns) { var index = this.options.ns.indexOf(ns); if (index > -1) { this.options.ns.splice(index, 1); } } }, { key: "getResource", value: function getResource(lng, ns, key) { var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; var keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; var path = [lng, ns]; if (key && typeof key !== 'string') path = path.concat(key); if (key && typeof key === 'string') path = path.concat(keySeparator ? key.split(keySeparator) : key); if (lng.indexOf('.') > -1) { path = lng.split('.'); } return getPath(this.data, path); } }, { key: "addResource", value: function addResource(lng, ns, key, value) { var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : { silent: false }; var keySeparator = this.options.keySeparator; if (keySeparator === undefined) keySeparator = '.'; var path = [lng, ns]; if (key) path = path.concat(keySeparator ? key.split(keySeparator) : key); if (lng.indexOf('.') > -1) { path = lng.split('.'); value = ns; ns = path[1]; } this.addNamespaces(ns); setPath(this.data, path, value); if (!options.silent) this.emit('added', lng, ns, key, value); } }, { key: "addResources", value: function addResources(lng, ns, resources) { var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : { silent: false }; for (var m in resources) { if (typeof resources[m] === 'string' || Object.prototype.toString.apply(resources[m]) === '[object Array]') this.addResource(lng, ns, m, resources[m], { silent: true }); } if (!options.silent) this.emit('added', lng, ns, resources); } }, { key: "addResourceBundle", value: function addResourceBundle(lng, ns, resources, deep, overwrite) { var options = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : { silent: false }; var path = [lng, ns]; if (lng.indexOf('.') > -1) { path = lng.split('.'); deep = resources; resources = ns; ns = path[1]; } this.addNamespaces(ns); var pack = getPath(this.data, path) || {}; if (deep) { deepExtend(pack, resources, overwrite); } else { pack = _objectSpread({}, pack, resources); } setPath(this.data, path, pack); if (!options.silent) this.emit('added', lng, ns, resources); } }, { key: "removeResourceBundle", value: function removeResourceBundle(lng, ns) { if (this.hasResourceBundle(lng, ns)) { delete this.data[lng][ns]; } this.removeNamespaces(ns); this.emit('removed', lng, ns); } }, { key: "hasResourceBundle", value: function hasResourceBundle(lng, ns) { return this.getResource(lng, ns) !== undefined; } }, { key: "getResourceBundle", value: function getResourceBundle(lng, ns) { if (!ns) ns = this.options.defaultNS; if (this.options.compatibilityAPI === 'v1') return _objectSpread({}, {}, this.getResource(lng, ns)); return this.getResource(lng, ns); } }, { key: "getDataByLanguage", value: function getDataByLanguage(lng) { return this.data[lng]; } }, { key: "toJSON", value: function toJSON() { return this.data; } }]); return ResourceStore; }(EventEmitter); var postProcessor = { processors: {}, addPostProcessor: function addPostProcessor(module) { this.processors[module.name] = module; }, handle: function handle(processors, value, key, options, translator) { var _this = this; processors.forEach(function (processor) { if (_this.processors[processor]) value = _this.processors[processor].process(value, key, options, translator); }); return value; } }; var checkedLoadedFor = {}; var Translator = function (_EventEmitter) { _inherits(Translator, _EventEmitter); function Translator(services) { var _this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, Translator); _this = _possibleConstructorReturn(this, _getPrototypeOf(Translator).call(this)); if (isIE10) { EventEmitter.call(_assertThisInitialized(_this)); } copy(['resourceStore', 'languageUtils', 'pluralResolver', 'interpolator', 'backendConnector', 'i18nFormat', 'utils'], services, _assertThisInitialized(_this)); _this.options = options; if (_this.options.keySeparator === undefined) { _this.options.keySeparator = '.'; } _this.logger = baseLogger.create('translator'); return _this; } _createClass(Translator, [{ key: "changeLanguage", value: function changeLanguage(lng) { if (lng) this.language = lng; } }, { key: "exists", value: function exists(key) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { interpolation: {} }; var resolved = this.resolve(key, options); return resolved && resolved.res !== undefined; } }, { key: "extractFromKey", value: function extractFromKey(key, options) { var nsSeparator = options.nsSeparator !== undefined ? options.nsSeparator : this.options.nsSeparator; if (nsSeparator === undefined) nsSeparator = ':'; var keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; var namespaces = options.ns || this.options.defaultNS; if (nsSeparator && key.indexOf(nsSeparator) > -1) { var m = key.match(this.interpolator.nestingRegexp); if (m && m.length > 0) { return { key: key, namespaces: namespaces }; } var parts = key.split(nsSeparator); if (nsSeparator !== keySeparator || nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1) namespaces = parts.shift(); key = parts.join(keySeparator); } if (typeof namespaces === 'string') namespaces = [namespaces]; return { key: key, namespaces: namespaces }; } }, { key: "translate", value: function translate(keys, options, lastKey) { var _this2 = this; if (_typeof(options) !== 'object' && this.options.overloadTranslationOptionHandler) { options = this.options.overloadTranslationOptionHandler(arguments); } if (!options) options = {}; if (keys === undefined || keys === null) return ''; if (!Array.isArray(keys)) keys = [String(keys)]; var keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; var _this$extractFromKey = this.extractFromKey(keys[keys.length - 1], options), key = _this$extractFromKey.key, namespaces = _this$extractFromKey.namespaces; var namespace = namespaces[namespaces.length - 1]; var lng = options.lng || this.language; var appendNamespaceToCIMode = options.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode; if (lng && lng.toLowerCase() === 'cimode') { if (appendNamespaceToCIMode) { var nsSeparator = options.nsSeparator || this.options.nsSeparator; return namespace + nsSeparator + key; } return key; } var resolved = this.resolve(keys, options); var res = resolved && resolved.res; var resUsedKey = resolved && resolved.usedKey || key; var resExactUsedKey = resolved && resolved.exactUsedKey || key; var resType = Object.prototype.toString.apply(res); var noObject = ['[object Number]', '[object Function]', '[object RegExp]']; var joinArrays = options.joinArrays !== undefined ? options.joinArrays : this.options.joinArrays; var handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject; var handleAsObject = typeof res !== 'string' && typeof res !== 'boolean' && typeof res !== 'number'; if (handleAsObjectInI18nFormat && res && handleAsObject && noObject.indexOf(resType) < 0 && !(typeof joinArrays === 'string' && resType === '[object Array]')) { if (!options.returnObjects && !this.options.returnObjects) { this.logger.warn('accessing an object - but returnObjects options is not enabled!'); return this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, res, options) : "key '".concat(key, " (").concat(this.language, ")' returned an object instead of string."); } if (keySeparator) { var resTypeIsArray = resType === '[object Array]'; var copy = resTypeIsArray ? [] : {}; var newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey; for (var m in res) { if (Object.prototype.hasOwnProperty.call(res, m)) { var deepKey = "".concat(newKeyToUse).concat(keySeparator).concat(m); copy[m] = this.translate(deepKey, _objectSpread({}, options, { joinArrays: false, ns: namespaces })); if (copy[m] === deepKey) copy[m] = res[m]; } } res = copy; } } else if (handleAsObjectInI18nFormat && typeof joinArrays === 'string' && resType === '[object Array]') { res = res.join(joinArrays); if (res) res = this.extendTranslation(res, keys, options, lastKey); } else { var usedDefault = false; var usedKey = false; var needsPluralHandling = options.count !== undefined && typeof options.count !== 'string'; var hasDefaultValue = Translator.hasDefaultValue(options); var defaultValueSuffix = needsPluralHandling ? this.pluralResolver.getSuffix(lng, options.count) : ''; var defaultValue = options["defaultValue".concat(defaultValueSuffix)] || options.defaultValue; if (!this.isValidLookup(res) && hasDefaultValue) { usedDefault = true; res = defaultValue; } if (!this.isValidLookup(res)) { usedKey = true; res = key; } var updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing; if (usedKey || usedDefault || updateMissing) { this.logger.log(updateMissing ? 'updateKey' : 'missingKey', lng, namespace, key, updateMissing ? defaultValue : res); if (keySeparator) { var fk = this.resolve(key, _objectSpread({}, options, { keySeparator: false })); if (fk && fk.res) this.logger.warn('Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.'); } var lngs = []; var fallbackLngs = this.languageUtils.getFallbackCodes(this.options.fallbackLng, options.lng || this.language); if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) { for (var i = 0; i < fallbackLngs.length; i++) { lngs.push(fallbackLngs[i]); } } else if (this.options.saveMissingTo === 'all') { lngs = this.languageUtils.toResolveHierarchy(options.lng || this.language); } else { lngs.push(options.lng || this.language); } var send = function send(l, k, fallbackValue) { if (_this2.options.missingKeyHandler) { _this2.options.missingKeyHandler(l, namespace, k, updateMissing ? fallbackValue : res, updateMissing, options); } else if (_this2.backendConnector && _this2.backendConnector.saveMissing) { _this2.backendConnector.saveMissing(l, namespace, k, updateMissing ? fallbackValue : res, updateMissing, options); } _this2.emit('missingKey', l, namespace, k, res); }; if (this.options.saveMissing) { if (this.options.saveMissingPlurals && needsPluralHandling) { lngs.forEach(function (language) { _this2.pluralResolver.getSuffixes(language).forEach(function (suffix) { send([language], key + suffix, options["defaultValue".concat(suffix)] || defaultValue); }); }); } else { send(lngs, key, defaultValue); } } } res = this.extendTranslation(res, keys, options, resolved, lastKey); if (usedKey && res === key && this.options.appendNamespaceToMissingKey) res = "".concat(namespace, ":").concat(key); if (usedKey && this.options.parseMissingKeyHandler) res = this.options.parseMissingKeyHandler(res); } return res; } }, { key: "extendTranslation", value: function extendTranslation(res, key, options, resolved, lastKey) { var _this3 = this; if (this.i18nFormat && this.i18nFormat.parse) { res = this.i18nFormat.parse(res, options, resolved.usedLng, resolved.usedNS, resolved.usedKey, { resolved: resolved }); } else if (!options.skipInterpolation) { if (options.interpolation) this.interpolator.init(_objectSpread({}, options, { interpolation: _objectSpread({}, this.options.interpolation, options.interpolation) })); var skipOnVariables = options.interpolation && options.interpolation.skipOnVariables || this.options.interpolation.skipOnVariables; var nestBef; if (skipOnVariables) { var nb = res.match(this.interpolator.nestingRegexp); nestBef = nb && nb.length; } var data = options.replace && typeof options.replace !== 'string' ? options.replace : options; if (this.options.interpolation.defaultVariables) data = _objectSpread({}, this.options.interpolation.defaultVariables, data); res = this.interpolator.interpolate(res, data, options.lng || this.language, options); if (skipOnVariables) { var na = res.match(this.interpolator.nestingRegexp); var nestAft = na && na.length; if (nestBef < nestAft) options.nest = false; } if (options.nest !== false) res = this.interpolator.nest(res, function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } if (lastKey && lastKey[0] === args[0] && !options.context) { _this3.logger.warn("It seems you are nesting recursively key: ".concat(args[0], " in key: ").concat(key[0])); return null; } return _this3.translate.apply(_this3, args.concat([key])); }, options); if (options.interpolation) this.interpolator.reset(); } var postProcess = options.postProcess || this.options.postProcess; var postProcessorNames = typeof postProcess === 'string' ? [postProcess] : postProcess; if (res !== undefined && res !== null && postProcessorNames && postProcessorNames.length && options.applyPostProcessor !== false) { res = postProcessor.handle(postProcessorNames, res, key, this.options && this.options.postProcessPassResolved ? _objectSpread({ i18nResolved: resolved }, options) : options, this); } return res; } }, { key: "resolve", value: function resolve(keys) { var _this4 = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var found; var usedKey; var exactUsedKey; var usedLng; var usedNS; if (typeof keys === 'string') keys = [keys]; keys.forEach(function (k) { if (_this4.isValidLookup(found)) return; var extracted = _this4.extractFromKey(k, options); var key = extracted.key; usedKey = key; var namespaces = extracted.namespaces; if (_this4.options.fallbackNS) namespaces = namespaces.concat(_this4.options.fallbackNS); var needsPluralHandling = options.count !== undefined && typeof options.count !== 'string'; var needsContextHandling = options.context !== undefined && typeof options.context === 'string' && options.context !== ''; var codes = options.lngs ? options.lngs : _this4.languageUtils.toResolveHierarchy(options.lng || _this4.language, options.fallbackLng); namespaces.forEach(function (ns) { if (_this4.isValidLookup(found)) return; usedNS = ns; if (!checkedLoadedFor["".concat(codes[0], "-").concat(ns)] && _this4.utils && _this4.utils.hasLoadedNamespace && !_this4.utils.hasLoadedNamespace(usedNS)) { checkedLoadedFor["".concat(codes[0], "-").concat(ns)] = true; _this4.logger.warn("key \"".concat(usedKey, "\" for languages \"").concat(codes.join(', '), "\" won't get resolved as namespace \"").concat(usedNS, "\" was not yet loaded"), 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!'); } codes.forEach(function (code) { if (_this4.isValidLookup(found)) return; usedLng = code; var finalKey = key; var finalKeys = [finalKey]; if (_this4.i18nFormat && _this4.i18nFormat.addLookupKeys) { _this4.i18nFormat.addLookupKeys(finalKeys, key, code, ns, options); } else { var pluralSuffix; if (needsPluralHandling) pluralSuffix = _this4.pluralResolver.getSuffix(code, options.count); if (needsPluralHandling && needsContextHandling) finalKeys.push(finalKey + pluralSuffix); if (needsContextHandling) finalKeys.push(finalKey += "".concat(_this4.options.contextSeparator).concat(options.context)); if (needsPluralHandling) finalKeys.push(finalKey += pluralSuffix); } var possibleKey; while (possibleKey = finalKeys.pop()) { if (!_this4.isValidLookup(found)) { exactUsedKey = possibleKey; found = _this4.getResource(code, ns, possibleKey, options); } } }); }); }); return { res: found, usedKey: usedKey, exactUsedKey: exactUsedKey, usedLng: usedLng, usedNS: usedNS }; } }, { key: "isValidLookup", value: function isValidLookup(res) { return res !== undefined && !(!this.options.returnNull && res === null) && !(!this.options.returnEmptyString && res === ''); } }, { key: "getResource", value: function getResource(code, ns, key) { var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; if (this.i18nFormat && this.i18nFormat.getResource) return this.i18nFormat.getResource(code, ns, key, options); return this.resourceStore.getResource(code, ns, key, options); } }], [{ key: "hasDefaultValue", value: function hasDefaultValue(options) { var prefix = 'defaultValue'; for (var option in options) { if (Object.prototype.hasOwnProperty.call(options, option) && prefix === option.substring(0, prefix.length) && undefined !== options[option]) { return true; } } return false; } }]); return Translator; }(EventEmitter); function capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1); } var LanguageUtil = function () { function LanguageUtil(options) { _classCallCheck(this, LanguageUtil); this.options = options; this.whitelist = this.options.supportedLngs || false; this.supportedLngs = this.options.supportedLngs || false; this.logger = baseLogger.create('languageUtils'); } _createClass(LanguageUtil, [{ key: "getScriptPartFromCode", value: function getScriptPartFromCode(code) { if (!code || code.indexOf('-') < 0) return null; var p = code.split('-'); if (p.length === 2) return null; p.pop(); if (p[p.length - 1].toLowerCase() === 'x') return null; return this.formatLanguageCode(p.join('-')); } }, { key: "getLanguagePartFromCode", value: function getLanguagePartFromCode(code) { if (!code || code.indexOf('-') < 0) return code; var p = code.split('-'); return this.formatLanguageCode(p[0]); } }, { key: "formatLanguageCode", value: function formatLanguageCode(code) { if (typeof code === 'string' && code.indexOf('-') > -1) { var specialCases = ['hans', 'hant', 'latn', 'cyrl', 'cans', 'mong', 'arab']; var p = code.split('-'); if (this.options.lowerCaseLng) { p = p.map(function (part) { return part.toLowerCase(); }); } else if (p.length === 2) { p[0] = p[0].toLowerCase(); p[1] = p[1].toUpperCase(); if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase()); } else if (p.length === 3) { p[0] = p[0].toLowerCase(); if (p[1].length === 2) p[1] = p[1].toUpperCase(); if (p[0] !== 'sgn' && p[2].length === 2) p[2] = p[2].toUpperCase(); if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase()); if (specialCases.indexOf(p[2].toLowerCase()) > -1) p[2] = capitalize(p[2].toLowerCase()); } return p.join('-'); } return this.options.cleanCode || this.options.lowerCaseLng ? code.toLowerCase() : code; } }, { key: "isWhitelisted", value: function isWhitelisted(code) { this.logger.deprecate('languageUtils.isWhitelisted', 'function "isWhitelisted" will be renamed to "isSupportedCode" in the next major - please make sure to rename it\'s usage asap.'); return this.isSupportedCode(code); } }, { key: "isSupportedCode", value: function isSupportedCode(code) { if (this.options.load === 'languageOnly' || this.options.nonExplicitSupportedLngs) { code = this.getLanguagePartFromCode(code); } return !this.supportedLngs || !this.supportedLngs.length || this.supportedLngs.indexOf(code) > -1; } }, { key: "getBestMatchFromCodes", value: function getBestMatchFromCodes(codes) { var _this = this; if (!codes) return null; var found; codes.forEach(function (code) { if (found) return; var cleanedLng = _this.formatLanguageCode(code); if (!_this.options.supportedLngs || _this.isSupportedCode(cleanedLng)) found = cleanedLng; }); if (!found && this.options.supportedLngs) { codes.forEach(function (code) { if (found) return; var lngOnly = _this.getLanguagePartFromCode(code); if (_this.isSupportedCode(lngOnly)) return found = lngOnly; found = _this.options.supportedLngs.find(function (supportedLng) { if (supportedLng.indexOf(lngOnly) === 0) return supportedLng; }); }); } if (!found) found = this.getFallbackCodes(this.options.fallbackLng)[0]; return found; } }, { key: "getFallbackCodes", value: function getFallbackCodes(fallbacks, code) { if (!fallbacks) return []; if (typeof fallbacks === 'function') fallbacks = fallbacks(code); if (typeof fallbacks === 'string') fallbacks = [fallbacks]; if (Object.prototype.toString.apply(fallbacks) === '[object Array]') return fallbacks; if (!code) return fallbacks["default"] || []; var found = fallbacks[code]; if (!found) found = fallbacks[this.getScriptPartFromCode(code)]; if (!found) found = fallbacks[this.formatLanguageCode(code)]; if (!found) found = fallbacks[this.getLanguagePartFromCode(code)]; if (!found) found = fallbacks["default"]; return found || []; } }, { key: "toResolveHierarchy", value: function toResolveHierarchy(code, fallbackCode) { var _this2 = this; var fallbackCodes = this.getFallbackCodes(fallbackCode || this.options.fallbackLng || [], code); var codes = []; var addCode = function addCode(c) { if (!c) return; if (_this2.isSupportedCode(c)) { codes.push(c); } else { _this2.logger.warn("rejecting language code not found in supportedLngs: ".concat(c)); } }; if (typeof code === 'string' && code.indexOf('-') > -1) { if (this.options.load !== 'languageOnly') addCode(this.formatLanguageCode(code)); if (this.options.load !== 'languageOnly' && this.options.load !== 'currentOnly') addCode(this.getScriptPartFromCode(code)); if (this.options.load !== 'currentOnly') addCode(this.getLanguagePartFromCode(code)); } else if (typeof code === 'string') { addCode(this.formatLanguageCode(code)); } fallbackCodes.forEach(function (fc) { if (codes.indexOf(fc) < 0) addCode(_this2.formatLanguageCode(fc)); }); return codes; } }]); return LanguageUtil; }(); var sets = [{ lngs: ['ach', 'ak', 'am', 'arn', 'br', 'fil', 'gun', 'ln', 'mfe', 'mg', 'mi', 'oc', 'pt', 'pt-BR', 'tg', 'tl', 'ti', 'tr', 'uz', 'wa'], nr: [1, 2], fc: 1 }, { lngs: ['af', 'an', 'ast', 'az', 'bg', 'bn', 'ca', 'da', 'de', 'dev', 'el', 'en', 'eo', 'es', 'et', 'eu', 'fi', 'fo', 'fur', 'fy', 'gl', 'gu', 'ha', 'hi', 'hu', 'hy', 'ia', 'it', 'kn', 'ku', 'lb', 'mai', 'ml', 'mn', 'mr', 'nah', 'nap', 'nb', 'ne', 'nl', 'nn', 'no', 'nso', 'pa', 'pap', 'pms', 'ps', 'pt-PT', 'rm', 'sco', 'se', 'si', 'so', 'son', 'sq', 'sv', 'sw', 'ta', 'te', 'tk', 'ur', 'yo'], nr: [1, 2], fc: 2 }, { lngs: ['ay', 'bo', 'cgg', 'fa', 'ht', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky', 'lo', 'ms', 'sah', 'su', 'th', 'tt', 'ug', 'vi', 'wo', 'zh'], nr: [1], fc: 3 }, { lngs: ['be', 'bs', 'cnr', 'dz', 'hr', 'ru', 'sr', 'uk'], nr: [1, 2, 5], fc: 4 }, { lngs: ['ar'], nr: [0, 1, 2, 3, 11, 100], fc: 5 }, { lngs: ['cs', 'sk'], nr: [1, 2, 5], fc: 6 }, { lngs: ['csb', 'pl'], nr: [1, 2, 5], fc: 7 }, { lngs: ['cy'], nr: [1, 2, 3, 8], fc: 8 }, { lngs: ['fr'], nr: [1, 2], fc: 9 }, { lngs: ['ga'], nr: [1, 2, 3, 7, 11], fc: 10 }, { lngs: ['gd'], nr: [1, 2, 3, 20], fc: 11 }, { lngs: ['is'], nr: [1, 2], fc: 12 }, { lngs: ['jv'], nr: [0, 1], fc: 13 }, { lngs: ['kw'], nr: [1, 2, 3, 4], fc: 14 }, { lngs: ['lt'], nr: [1, 2, 10], fc: 15 }, { lngs: ['lv'], nr: [1, 2, 0], fc: 16 }, { lngs: ['mk'], nr: [1, 2], fc: 17 }, { lngs: ['mnk'], nr: [0, 1, 2], fc: 18 }, { lngs: ['mt'], nr: [1, 2, 11, 20], fc: 19 }, { lngs: ['or'], nr: [2, 1], fc: 2 }, { lngs: ['ro'], nr: [1, 2, 20], fc: 20 }, { lngs: ['sl'], nr: [5, 1, 2, 3], fc: 21 }, { lngs: ['he', 'iw'], nr: [1, 2, 20, 21], fc: 22 }]; var _rulesPluralsTypes = { 1: function _(n) { return Number(n > 1); }, 2: function _(n) { return Number(n != 1); }, 3: function _(n) { return 0; }, 4: function _(n) { return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); }, 5: function _(n) { return Number(n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5); }, 6: function _(n) { return Number(n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2); }, 7: function _(n) { return Number(n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); }, 8: function _(n) { return Number(n == 1 ? 0 : n == 2 ? 1 : n != 8 && n != 11 ? 2 : 3); }, 9: function _(n) { return Number(n >= 2); }, 10: function _(n) { return Number(n == 1 ? 0 : n == 2 ? 1 : n < 7 ? 2 : n < 11 ? 3 : 4); }, 11: function _(n) { return Number(n == 1 || n == 11 ? 0 : n == 2 || n == 12 ? 1 : n > 2 && n < 20 ? 2 : 3); }, 12: function _(n) { return Number(n % 10 != 1 || n % 100 == 11); }, 13: function _(n) { return Number(n !== 0); }, 14: function _(n) { return Number(n == 1 ? 0 : n == 2 ? 1 : n == 3 ? 2 : 3); }, 15: function _(n) { return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); }, 16: function _(n) { return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n !== 0 ? 1 : 2); }, 17: function _(n) { return Number(n == 1 || n % 10 == 1 && n % 100 != 11 ? 0 : 1); }, 18: function _(n) { return Number(n == 0 ? 0 : n == 1 ? 1 : 2); }, 19: function _(n) { return Number(n == 1 ? 0 : n == 0 || n % 100 > 1 && n % 100 < 11 ? 1 : n % 100 > 10 && n % 100 < 20 ? 2 : 3); }, 20: function _(n) { return Number(n == 1 ? 0 : n == 0 || n % 100 > 0 && n % 100 < 20 ? 1 : 2); }, 21: function _(n) { return Number(n % 100 == 1 ? 1 : n % 100 == 2 ? 2 : n % 100 == 3 || n % 100 == 4 ? 3 : 0); }, 22: function _(n) { return Number(n == 1 ? 0 : n == 2 ? 1 : (n < 0 || n > 10) && n % 10 == 0 ? 2 : 3); } }; function createRules() { var rules = {}; sets.forEach(function (set) { set.lngs.forEach(function (l) { rules[l] = { numbers: set.nr, plurals: _rulesPluralsTypes[set.fc] }; }); }); return rules; } var PluralResolver = function () { function PluralResolver(languageUtils) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, PluralResolver); this.languageUtils = languageUtils; this.options = options; this.logger = baseLogger.create('pluralResolver'); this.rules = createRules(); } _createClass(PluralResolver, [{ key: "addRule", value: function addRule(lng, obj) { this.rules[lng] = obj; } }, { key: "getRule", value: function getRule(code) { return this.rules[code] || this.rules[this.languageUtils.getLanguagePartFromCode(code)]; } }, { key: "needsPlural", value: function needsPlural(code) { var rule = this.getRule(code); return rule && rule.numbers.length > 1; } }, { key: "getPluralFormsOfKey", value: function getPluralFormsOfKey(code, key) { return this.getSuffixes(code).map(function (suffix) { return key + suffix; }); } }, { key: "getSuffixes", value: function getSuffixes(code) { var _this = this; var rule = this.getRule(code); if (!rule) { return []; } return rule.numbers.map(function (number) { return _this.getSuffix(code, number); }); } }, { key: "getSuffix", value: function getSuffix(code, count) { var _this2 = this; var rule = this.getRule(code); if (rule) { var idx = rule.noAbs ? rule.plurals(count) : rule.plurals(Math.abs(count)); var suffix = rule.numbers[idx]; if (this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) { if (suffix === 2) { suffix = 'plural'; } else if (suffix === 1) { suffix = ''; } } var returnSuffix = function returnSuffix() { return _this2.options.prepend && suffix.toString() ? _this2.options.prepend + suffix.toString() : suffix.toString(); }; if (this.options.compatibilityJSON === 'v1') { if (suffix === 1) return ''; if (typeof suffix === 'number') return "_plural_".concat(suffix.toString()); return returnSuffix(); } else if (this.options.compatibilityJSON === 'v2') { return returnSuffix(); } else if (this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) { return returnSuffix(); } return this.options.prepend && idx.toString() ? this.options.prepend + idx.toString() : idx.toString(); } this.logger.warn("no plural rule found for: ".concat(code)); return ''; } }]); return PluralResolver; }(); var Interpolator = function () { function Interpolator() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _classCallCheck(this, Interpolator); this.logger = baseLogger.create('interpolator'); this.options = options; this.format = options.interpolation && options.interpolation.format || function (value) { return value; }; this.init(options); } _createClass(Interpolator, [{ key: "init", value: function init() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (!options.interpolation) options.interpolation = { escapeValue: true }; var iOpts = options.interpolation; this.escape = iOpts.escape !== undefined ? iOpts.escape : escape; this.escapeValue = iOpts.escapeValue !== undefined ? iOpts.escapeValue : true; this.useRawValueToEscape = iOpts.useRawValueToEscape !== undefined ? iOpts.useRawValueToEscape : false; this.prefix = iOpts.prefix ? regexEscape(iOpts.prefix) : iOpts.prefixEscaped || '{{'; this.suffix = iOpts.suffix ? regexEscape(iOpts.suffix) : iOpts.suffixEscaped || '}}'; this.formatSeparator = iOpts.formatSeparator ? iOpts.formatSeparator : iOpts.formatSeparator || ','; this.unescapePrefix = iOpts.unescapeSuffix ? '' : iOpts.unescapePrefix || '-'; this.unescapeSuffix = this.unescapePrefix ? '' : iOpts.unescapeSuffix || ''; this.nestingPrefix = iOpts.nestingPrefix ? regexEscape(iOpts.nestingPrefix) : iOpts.nestingPrefixEscaped || regexEscape('$t('); this.nestingSuffix = iOpts.nestingSuffix ? regexEscape(iOpts.nestingSuffix) : iOpts.nestingSuffixEscaped || regexEscape(')'); this.nestingOptionsSeparator = iOpts.nestingOptionsSeparator ? iOpts.nestingOptionsSeparator : iOpts.nestingOptionsSeparator || ','; this.maxReplaces = iOpts.maxReplaces ? iOpts.maxReplaces : 1000; this.alwaysFormat = iOpts.alwaysFormat !== undefined ? iOpts.alwaysFormat : false; this.resetRegExp(); } }, { key: "reset", value: function reset() { if (this.options) this.init(this.options); } }, { key: "resetRegExp", value: function resetRegExp() { var regexpStr = "".concat(this.prefix, "(.+?)").concat(this.suffix); this.regexp = new RegExp(regexpStr, 'g'); var regexpUnescapeStr = "".concat(this.prefix).concat(this.unescapePrefix, "(.+?)").concat(this.unescapeSuffix).concat(this.suffix); this.regexpUnescape = new RegExp(regexpUnescapeStr, 'g'); var nestingRegexpStr = "".concat(this.nestingPrefix, "(.+?)").concat(this.nestingSuffix); this.nestingRegexp = new RegExp(nestingRegexpStr, 'g'); } }, { key: "interpolate", value: function interpolate(str, data, lng, options) { var _this = this; var match; var value; var replaces; var defaultData = this.options && this.options.interpolation && this.options.interpolation.defaultVariables || {}; function regexSafe(val) { return val.replace(/\$/g, '$$$$'); } var handleFormat = function handleFormat(key) { if (key.indexOf(_this.formatSeparator) < 0) { var path = getPathWithDefaults(data, defaultData, key); return _this.alwaysFormat ? _this.format(path, undefined, lng) : path; } var p = key.split(_this.formatSeparator); var k = p.shift().trim(); var f = p.join(_this.formatSeparator).trim(); return _this.format(getPathWithDefaults(data, defaultData, k), f, lng, options); }; this.resetRegExp(); var missingInterpolationHandler = options && options.missingInterpolationHandler || this.options.missingInterpolationHandler; var skipOnVariables = options && options.interpolation && options.interpolation.skipOnVariables || this.options.interpolation.skipOnVariables; var todos = [{ regex: this.regexpUnescape, safeValue: function safeValue(val) { return regexSafe(val); } }, { regex: this.regexp, safeValue: function safeValue(val) { return _this.escapeValue ? regexSafe(_this.escape(val)) : regexSafe(val); } }]; todos.forEach(function (todo) { replaces = 0; while (match = todo.regex.exec(str)) { value = handleFormat(match[1].trim()); if (value === undefined) { if (typeof missingInterpolationHandler === 'function') { var temp = missingInterpolationHandler(str, match, options); value = typeof temp === 'string' ? temp : ''; } else if (skipOnVariables) { value = match[0]; continue; } else { _this.logger.warn("missed to pass in variable ".concat(match[1], " for interpolating ").concat(str)); value = ''; } } else if (typeof value !== 'string' && !_this.useRawValueToEscape) { value = makeString(value); } str = str.replace(match[0], todo.safeValue(value)); todo.regex.lastIndex = 0; replaces++; if (replaces >= _this.maxReplaces) { break; } } }); return str; } }, { key: "nest", value: function nest(str, fc) { var _this2 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var match; var value; var clonedOptions = _objectSpread({}, options); clonedOptions.applyPostProcessor = false; delete clonedOptions.defaultValue; function handleHasOptions(key, inheritedOptions) { var sep = this.nestingOptionsSeparator; if (key.indexOf(sep) < 0) return key; var c = key.split(new RegExp("".concat(sep, "[ ]*{"))); var optionsString = "{".concat(c[1]); key = c[0]; optionsString = this.interpolate(optionsString, clonedOptions); optionsString = optionsString.replace(/'/g, '"'); try { clonedOptions = JSON.parse(optionsString); if (inheritedOptions) clonedOptions = _objectSpread({}, inheritedOptions, clonedOptions); } catch (e) { this.logger.warn("failed parsing options string in nesting for key ".concat(key), e); return "".concat(key).concat(sep).concat(optionsString); } delete clonedOptions.defaultValue; return key; } while (match = this.nestingRegexp.exec(str)) { var formatters = []; var doReduce = false; if (match[0].includes(this.formatSeparator) && !/{.*}/.test(match[1])) { var r = match[1].split(this.formatSeparator).map(function (elem) { return elem.trim(); }); match[1] = r.shift(); formatters = r; doReduce = true; } value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions); if (value && match[0] === str && typeof value !== 'string') return value; if (typeof value !== 'string') value = makeString(value); if (!value) { this.logger.warn("missed to resolve ".concat(match[1], " for nesting ").concat(str)); value = ''; } if (doReduce) { value = formatters.reduce(function (v, f) { return _this2.format(v, f, options.lng, options); }, value.trim()); } str = str.replace(match[0], value); this.regexp.lastIndex = 0; } return str; } }]); return Interpolator; }(); function remove(arr, what) { var found = arr.indexOf(what); while (found !== -1) { arr.splice(found, 1); found = arr.indexOf(what); } } var Connector = function (_EventEmitter) { _inherits(Connector, _EventEmitter); function Connector(backend, store, services) { var _this; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; _classCallCheck(this, Connector); _this = _possibleConstructorReturn(this, _getPrototypeOf(Connector).call(this)); if (isIE10) { EventEmitter.call(_assertThisInitialized(_this)); } _this.backend = backend; _this.store = store; _this.services = services; _this.languageUtils = services.languageUtils; _this.options = options; _this.logger = baseLogger.create('backendConnector'); _this.state = {}; _this.queue = []; if (_this.backend && _this.backend.init) { _this.backend.init(services, options.backend, options); } return _this; } _createClass(Connector, [{ key: "queueLoad", value: function queueLoad(languages, namespaces, options, callback) { var _this2 = this; var toLoad = []; var pending = []; var toLoadLanguages = []; var toLoadNamespaces = []; languages.forEach(function (lng) { var hasAllNamespaces = true; namespaces.forEach(function (ns) { var name = "".concat(lng, "|").concat(ns); if (!options.reload && _this2.store.hasResourceBundle(lng, ns)) { _this2.state[name] = 2; } else if (_this2.state[name] < 0) ; else if (_this2.state[name] === 1) { if (pending.indexOf(name) < 0) pending.push(name); } else { _this2.state[name] = 1; hasAllNamespaces = false; if (pending.indexOf(name) < 0) pending.push(name); if (toLoad.indexOf(name) < 0) toLoad.push(name); if (toLoadNamespaces.indexOf(ns) < 0) toLoadNamespaces.push(ns); } }); if (!hasAllNamespaces) toLoadLanguages.push(lng); }); if (toLoad.length || pending.length) { this.queue.push({ pending: pending, loaded: {}, errors: [], callback: callback }); } return { toLoad: toLoad, pending: pending, toLoadLanguages: toLoadLanguages, toLoadNamespaces: toLoadNamespaces }; } }, { key: "loaded", value: function loaded(name, err, data) { var s = name.split('|'); var lng = s[0]; var ns = s[1]; if (err) this.emit('failedLoading', lng, ns, err); if (data) { this.store.addResourceBundle(lng, ns, data); } this.state[name] = err ? -1 : 2; var loaded = {}; this.queue.forEach(function (q) { pushPath(q.loaded, [lng], ns); remove(q.pending, name); if (err) q.errors.push(err); if (q.pending.length === 0 && !q.done) { Object.keys(q.loaded).forEach(function (l) { if (!loaded[l]) loaded[l] = []; if (q.loaded[l].length) { q.loaded[l].forEach(function (ns) { if (loaded[l].indexOf(ns) < 0) loaded[l].push(ns); }); } }); q.done = true; if (q.errors.length) { q.callback(q.errors); } else { q.callback(); } } }); this.emit('loaded', loaded); this.queue = this.queue.filter(function (q) { return !q.done; }); } }, { key: "read", value: function read(lng, ns, fcName) { var _this3 = this; var tried = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; var wait = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 350; var callback = arguments.length > 5 ? arguments[5] : undefined; if (!lng.length) return callback(null, {}); return this.backend[fcName](lng, ns, function (err, data) { if (err && data && tried < 5) { setTimeout(function () { _this3.read.call(_this3, lng, ns, fcName, tried + 1, wait * 2, callback); }, wait); return; } callback(err, data); }); } }, { key: "prepareLoading", value: function prepareLoading(languages, namespaces) { var _this4 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var callback = arguments.length > 3 ? arguments[3] : undefined; if (!this.backend) { this.logger.warn('No backend was added via i18next.use. Will not load resources.'); return callback && callback(); } if (typeof languages === 'string') languages = this.languageUtils.toResolveHierarchy(languages); if (typeof namespaces === 'string') namespaces = [namespaces]; var toLoad = this.queueLoad(languages, namespaces, options, callback); if (!toLoad.toLoad.length) { if (!toLoad.pending.length) callback(); return null; } toLoad.toLoad.forEach(function (name) { _this4.loadOne(name); }); } }, { key: "load", value: function load(languages, namespaces, callback) { this.prepareLoading(languages, namespaces, {}, callback); } }, { key: "reload", value: function reload(languages, namespaces, callback) { this.prepareLoading(languages, namespaces, { reload: true }, callback); } }, { key: "loadOne", value: function loadOne(name) { var _this5 = this; var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var s = name.split('|'); var lng = s[0]; var ns = s[1]; this.read(lng, ns, 'read', undefined, undefined, function (err, data) { if (err) _this5.logger.warn("".concat(prefix, "loading namespace ").concat(ns, " for language ").concat(lng, " failed"), err); if (!err && data) _this5.logger.log("".concat(prefix, "loaded namespace ").concat(ns, " for language ").concat(lng), data); _this5.loaded(name, err, data); }); } }, { key: "saveMissing", value: function saveMissing(languages, namespace, key, fallbackValue, isUpdate) { var options = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {}; if (this.services.utils && this.services.utils.hasLoadedNamespace && !this.services.utils.hasLoadedNamespace(namespace)) { this.logger.warn("did not save key \"".concat(key, "\" as the namespace \"").concat(namespace, "\" was not yet loaded"), 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!'); return; } if (key === undefined || key === null || key === '') return; if (this.backend && this.backend.create) { this.backend.create(languages, namespace, key, fallbackValue, null, _objectSpread({}, options, { isUpdate: isUpdate })); } if (!languages || !languages[0]) return; this.store.addResource(languages[0], namespace, key, fallbackValue); } }]); return Connector; }(EventEmitter); function get() { return { debug: false, initImmediate: true, ns: ['translation'], defaultNS: ['translation'], fallbackLng: ['dev'], fallbackNS: false, whitelist: false, nonExplicitWhitelist: false, supportedLngs: false, nonExplicitSupportedLngs: false, load: 'all', preload: false, simplifyPluralSuffix: true, keySeparator: '.', nsSeparator: ':', pluralSeparator: '_', contextSeparator: '_', partialBundledLanguages: false, saveMissing: false, updateMissing: false, saveMissingTo: 'fallback', saveMissingPlurals: true, missingKeyHandler: false, missingInterpolationHandler: false, postProcess: false, postProcessPassResolved: false, returnNull: true, returnEmptyString: true, returnObjects: false, joinArrays: false, returnedObjectHandler: false, parseMissingKeyHandler: false, appendNamespaceToMissingKey: false, appendNamespaceToCIMode: false, overloadTranslationOptionHandler: function handle(args) { var ret = {}; if (_typeof(args[1]) === 'object') ret = args[1]; if (typeof args[1] === 'string') ret.defaultValue = args[1]; if (typeof args[2] === 'string') ret.tDescription = args[2]; if (_typeof(args[2]) === 'object' || _typeof(args[3]) === 'object') { var options = args[3] || args[2]; Object.keys(options).forEach(function (key) { ret[key] = options[key]; }); } return ret; }, interpolation: { escapeValue: true, format: function format(value, _format, lng, options) { return value; }, prefix: '{{', suffix: '}}', formatSeparator: ',', unescapePrefix: '-', nestingPrefix: '$t(', nestingSuffix: ')', nestingOptionsSeparator: ',', maxReplaces: 1000, skipOnVariables: false } }; } function transformOptions(options) { if (typeof options.ns === 'string') options.ns = [options.ns]; if (typeof options.fallbackLng === 'string') options.fallbackLng = [options.fallbackLng]; if (typeof options.fallbackNS === 'string') options.fallbackNS = [options.fallbackNS]; if (options.whitelist) { if (options.whitelist && options.whitelist.indexOf('cimode') < 0) { options.whitelist = options.whitelist.concat(['cimode']); } options.supportedLngs = options.whitelist; } if (options.nonExplicitWhitelist) { options.nonExplicitSupportedLngs = options.nonExplicitWhitelist; } if (options.supportedLngs && options.supportedLngs.indexOf('cimode') < 0) { options.supportedLngs = options.supportedLngs.concat(['cimode']); } return options; } function noop() {} var I18n = function (_EventEmitter) { _inherits(I18n, _EventEmitter); function I18n() { var _this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var callback = arguments.length > 1 ? arguments[1] : undefined; _classCallCheck(this, I18n); _this = _possibleConstructorReturn(this, _getPrototypeOf(I18n).call(this)); if (isIE10) { EventEmitter.call(_assertThisInitialized(_this)); } _this.options = transformOptions(options); _this.services = {}; _this.logger = baseLogger; _this.modules = { external: [] }; if (callback && !_this.isInitialized && !options.isClone) { if (!_this.options.initImmediate) { _this.init(options, callback); return _possibleConstructorReturn(_this, _assertThisInitialized(_this)); } setTimeout(function () { _this.init(options, callback); }, 0); } return _this; } _createClass(I18n, [{ key: "init", value: function init() { var _this2 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var callback = arguments.length > 1 ? arguments[1] : undefined; if (typeof options === 'function') { callback = options; options = {}; } if (options.whitelist && !options.supportedLngs) { this.logger.deprecate('whitelist', 'option "whitelist" will be renamed to "supportedLngs" in the next major - please make sure to rename this option asap.'); } if (options.nonExplicitWhitelist && !options.nonExplicitSupportedLngs) { this.logger.deprecate('whitelist', 'options "nonExplicitWhitelist" will be renamed to "nonExplicitSupportedLngs" in the next major - please make sure to rename this option asap.'); } this.options = _objectSpread({}, get(), this.options, transformOptions(options)); this.format = this.options.interpolation.format; if (!callback) callback = noop; function createClassOnDemand(ClassOrObject) { if (!ClassOrObject) return null; if (typeof ClassOrObject === 'function') return new ClassOrObject(); return ClassOrObject; } if (!this.options.isClone) { if (this.modules.logger) { baseLogger.init(createClassOnDemand(this.modules.logger), this.options); } else { baseLogger.init(null, this.options); } var lu = new LanguageUtil(this.options); this.store = new ResourceStore(this.options.resources, this.options); var s = this.services; s.logger = baseLogger; s.resourceStore = this.store; s.languageUtils = lu; s.pluralResolver = new PluralResolver(lu, { prepend: this.options.pluralSeparator, compatibilityJSON: this.options.compatibilityJSON, simplifyPluralSuffix: this.options.simplifyPluralSuffix }); s.interpolator = new Interpolator(this.options); s.utils = { hasLoadedNamespace: this.hasLoadedNamespace.bind(this) }; s.backendConnector = new Connector(createClassOnDemand(this.modules.backend), s.resourceStore, s, this.options); s.backendConnector.on('*', function (event) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } _this2.emit.apply(_this2, [event].concat(args)); }); if (this.modules.languageDetector) { s.languageDetector = createClassOnDemand(this.modules.languageDetector); s.languageDetector.init(s, this.options.detection, this.options); } if (this.modules.i18nFormat) { s.i18nFormat = createClassOnDemand(this.modules.i18nFormat); if (s.i18nFormat.init) s.i18nFormat.init(this); } this.translator = new Translator(this.services, this.options); this.translator.on('*', function (event) { for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } _this2.emit.apply(_this2, [event].concat(args)); }); this.modules.external.forEach(function (m) { if (m.init) m.init(_this2); }); } if (this.options.fallbackLng && !this.services.languageDetector && !this.options.lng) { var codes = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng); if (codes.length > 0 && codes[0] !== 'dev') this.options.lng = codes[0]; } if (!this.services.languageDetector && !this.options.lng) { this.logger.warn('init: no languageDetector is used and no lng is defined'); } var storeApi = ['getResource', 'hasResourceBundle', 'getResourceBundle', 'getDataByLanguage']; storeApi.forEach(function (fcName) { _this2[fcName] = function () { var _this2$store; return (_this2$store = _this2.store)[fcName].apply(_this2$store, arguments); }; }); var storeApiChained = ['addResource', 'addResources', 'addResourceBundle', 'removeResourceBundle']; storeApiChained.forEach(function (fcName) { _this2[fcName] = function () { var _this2$store2; (_this2$store2 = _this2.store)[fcName].apply(_this2$store2, arguments); return _this2; }; }); var deferred = defer(); var load = function load() { var finish = function finish(err, t) { if (_this2.isInitialized) _this2.logger.warn('init: i18next is already initialized. You should call init just once!'); _this2.isInitialized = true; if (!_this2.options.isClone) _this2.logger.log('initialized', _this2.options); _this2.emit('initialized', _this2.options); deferred.resolve(t); callback(err, t); }; if (_this2.languages && _this2.options.compatibilityAPI !== 'v1' && !_this2.isInitialized) return finish(null, _this2.t.bind(_this2)); _this2.changeLanguage(_this2.options.lng, finish); }; if (this.options.resources || !this.options.initImmediate) { load(); } else { setTimeout(load, 0); } return deferred; } }, { key: "loadResources", value: function loadResources(language) { var _this3 = this; var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; var usedCallback = callback; var usedLng = typeof language === 'string' ? language : this.language; if (typeof language === 'function') usedCallback = language; if (!this.options.resources || this.options.partialBundledLanguages) { if (usedLng && usedLng.toLowerCase() === 'cimode') return usedCallback(); var toLoad = []; var append = function append(lng) { if (!lng) return; var lngs = _this3.services.languageUtils.toResolveHierarchy(lng); lngs.forEach(function (l) { if (toLoad.indexOf(l) < 0) toLoad.push(l); }); }; if (!usedLng) { var fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng); fallbacks.forEach(function (l) { return append(l); }); } else { append(usedLng); } if (this.options.preload) { this.options.preload.forEach(function (l) { return append(l); }); } this.services.backendConnector.load(toLoad, this.options.ns, usedCallback); } else { usedCallback(null); } } }, { key: "reloadResources", value: function reloadResources(lngs, ns, callback) { var deferred = defer(); if (!lngs) lngs = this.languages; if (!ns) ns = this.options.ns; if (!callback) callback = noop; this.services.backendConnector.reload(lngs, ns, function (err) { deferred.resolve(); callback(err); }); return deferred; } }, { key: "use", value: function use(module) { if (!module) throw new Error('You are passing an undefined module! Please check the object you are passing to i18next.use()'); if (!module.type) throw new Error('You are passing a wrong module! Please check the object you are passing to i18next.use()'); if (module.type === 'backend') { this.modules.backend = module; } if (module.type === 'logger' || module.log && module.warn && module.error) { this.modules.logger = module; } if (module.type === 'languageDetector') { this.modules.languageDetector = module; } if (module.type === 'i18nFormat') { this.modules.i18nFormat = module; } if (module.type === 'postProcessor') { postProcessor.addPostProcessor(module); } if (module.type === '3rdParty') { this.modules.external.push(module); } return this; } }, { key: "changeLanguage", value: function changeLanguage(lng, callback) { var _this4 = this; this.isLanguageChangingTo = lng; var deferred = defer(); this.emit('languageChanging', lng); var done = function done(err, l) { if (l) { _this4.language = l; _this4.languages = _this4.services.languageUtils.toResolveHierarchy(l); _this4.translator.changeLanguage(l); _this4.isLanguageChangingTo = undefined; _this4.emit('languageChanged', l); _this4.logger.log('languageChanged', l); } else { _this4.isLanguageChangingTo = undefined; } deferred.resolve(function () { return _this4.t.apply(_this4, arguments); }); if (callback) callback(err, function () { return _this4.t.apply(_this4, arguments); }); }; var setLng = function setLng(lngs) { var l = typeof lngs === 'string' ? lngs : _this4.services.languageUtils.getBestMatchFromCodes(lngs); if (l) { if (!_this4.language) { _this4.language = l; _this4.languages = _this4.services.languageUtils.toResolveHierarchy(l); } if (!_this4.translator.language) _this4.translator.changeLanguage(l); if (_this4.services.languageDetector) _this4.services.languageDetector.cacheUserLanguage(l); } _this4.loadResources(l, function (err) { done(err, l); }); }; if (!lng && this.services.languageDetector && !this.services.languageDetector.async) { setLng(this.services.languageDetector.detect()); } else if (!lng && this.services.languageDetector && this.services.languageDetector.async) { this.services.languageDetector.detect(setLng); } else { setLng(lng); } return deferred; } }, { key: "getFixedT", value: function getFixedT(lng, ns) { var _this5 = this; var fixedT = function fixedT(key, opts) { var options; if (_typeof(opts) !== 'object') { for (var _len3 = arguments.length, rest = new Array(_len3 > 2 ? _len3 - 2 : 0), _key3 = 2; _key3 < _len3; _key3++) { rest[_key3 - 2] = arguments[_key3]; } options = _this5.options.overloadTranslationOptionHandler([key, opts].concat(rest)); } else { options = _objectSpread({}, opts); } options.lng = options.lng || fixedT.lng; options.lngs = options.lngs || fixedT.lngs; options.ns = options.ns || fixedT.ns; return _this5.t(key, options); }; if (typeof lng === 'string') { fixedT.lng = lng; } else { fixedT.lngs = lng; } fixedT.ns = ns; return fixedT; } }, { key: "t", value: function t() { var _this$translator; return this.translator && (_this$translator = this.translator).translate.apply(_this$translator, arguments); } }, { key: "exists", value: function exists() { var _this$translator2; return this.translator && (_this$translator2 = this.translator).exists.apply(_this$translator2, arguments); } }, { key: "setDefaultNamespace", value: function setDefaultNamespace(ns) { this.options.defaultNS = ns; } }, { key: "hasLoadedNamespace", value: function hasLoadedNamespace(ns) { var _this6 = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (!this.isInitialized) { this.logger.warn('hasLoadedNamespace: i18next was not initialized', this.languages); return false; } if (!this.languages || !this.languages.length) { this.logger.warn('hasLoadedNamespace: i18n.languages were undefined or empty', this.languages); return false; } var lng = this.languages[0]; var fallbackLng = this.options ? this.options.fallbackLng : false; var lastLng = this.languages[this.languages.length - 1]; if (lng.toLowerCase() === 'cimode') return true; var loadNotPending = function loadNotPending(l, n) { var loadState = _this6.services.backendConnector.state["".concat(l, "|").concat(n)]; return loadState === -1 || loadState === 2; }; if (options.precheck) { var preResult = options.precheck(this, loadNotPending); if (preResult !== undefined) return preResult; } if (this.hasResourceBundle(lng, ns)) return true; if (!this.services.backendConnector.backend) return true; if (loadNotPending(lng, ns) && (!fallbackLng || loadNotPending(lastLng, ns))) return true; return false; } }, { key: "loadNamespaces", value: function loadNamespaces(ns, callback) { var _this7 = this; var deferred = defer(); if (!this.options.ns) { callback && callback(); return Promise.resolve(); } if (typeof ns === 'string') ns = [ns]; ns.forEach(function (n) { if (_this7.options.ns.indexOf(n) < 0) _this7.options.ns.push(n); }); this.loadResources(function (err) { deferred.resolve(); if (callback) callback(err); }); return deferred; } }, { key: "loadLanguages", value: function loadLanguages(lngs, callback) { var deferred = defer(); if (typeof lngs === 'string') lngs = [lngs]; var preloaded = this.options.preload || []; var newLngs = lngs.filter(function (lng) { return preloaded.indexOf(lng) < 0; }); if (!newLngs.length) { if (callback) callback(); return Promise.resolve(); } this.options.preload = preloaded.concat(newLngs); this.loadResources(function (err) { deferred.resolve(); if (callback) callback(err); }); return deferred; } }, { key: "dir", value: function dir(lng) { if (!lng) lng = this.languages && this.languages.length > 0 ? this.languages[0] : this.language; if (!lng) return 'rtl'; var rtlLngs = ['ar', 'shu', 'sqr', 'ssh', 'xaa', 'yhd', 'yud', 'aao', 'abh', 'abv', 'acm', 'acq', 'acw', 'acx', 'acy', 'adf', 'ads', 'aeb', 'aec', 'afb', 'ajp', 'apc', 'apd', 'arb', 'arq', 'ars', 'ary', 'arz', 'auz', 'avl', 'ayh', 'ayl', 'ayn', 'ayp', 'bbz', 'pga', 'he', 'iw', 'ps', 'pbt', 'pbu', 'pst', 'prp', 'prd', 'ug', 'ur', 'ydd', 'yds', 'yih', 'ji', 'yi', 'hbo', 'men', 'xmn', 'fa', 'jpr', 'peo', 'pes', 'prs', 'dv', 'sam']; return rtlLngs.indexOf(this.services.languageUtils.getLanguagePartFromCode(lng)) >= 0 ? 'rtl' : 'ltr'; } }, { key: "createInstance", value: function createInstance() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var callback = arguments.length > 1 ? arguments[1] : undefined; return new I18n(options, callback); } }, { key: "cloneInstance", value: function cloneInstance() { var _this8 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; var mergedOptions = _objectSpread({}, this.options, options, { isClone: true }); var clone = new I18n(mergedOptions); var membersToCopy = ['store', 'services', 'language']; membersToCopy.forEach(function (m) { clone[m] = _this8[m]; }); clone.services = _objectSpread({}, this.services); clone.services.utils = { hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone) }; clone.translator = new Translator(clone.services, clone.options); clone.translator.on('*', function (event) { for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) { args[_key4 - 1] = arguments[_key4]; } clone.emit.apply(clone, [event].concat(args)); }); clone.init(mergedOptions, callback); clone.translator.options = clone.options; clone.translator.backendConnector.services.utils = { hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone) }; return clone; } }]); return I18n; }(EventEmitter); var i18next = new I18n(); var arr = []; var each = arr.forEach; var slice = arr.slice; function defaults(obj) { each.call(slice.call(arguments, 1), function (source) { if (source) { for (var prop in source) { if (obj[prop] === undefined) obj[prop] = source[prop]; } } }); return obj; } var cookie = { create: function create(name, value, minutes, domain) { var cookieOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : { path: '/' }; var expires; if (minutes) { var date = new Date(); date.setTime(date.getTime() + minutes * 60 * 1000); expires = '; expires=' + date.toUTCString(); } else expires = ''; domain = domain ? 'domain=' + domain + ';' : ''; cookieOptions = Object.keys(cookieOptions).reduce(function (acc, key) { return acc + ';' + key.replace(/([A-Z])/g, function ($1) { return '-' + $1.toLowerCase(); }) + '=' + cookieOptions[key]; }, ''); document.cookie = name + '=' + encodeURIComponent(value) + expires + ';' + domain + cookieOptions; }, read: function read(name) { var nameEQ = name + '='; var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) === ' ') { c = c.substring(1, c.length); } if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); } return null; }, remove: function remove(name) { this.create(name, '', -1); } }; var cookie$1 = { name: 'cookie', lookup: function lookup(options) { var found; if (options.lookupCookie && typeof document !== 'undefined') { var c = cookie.read(options.lookupCookie); if (c) found = c; } return found; }, cacheUserLanguage: function cacheUserLanguage(lng, options) { if (options.lookupCookie && typeof document !== 'undefined') { cookie.create(options.lookupCookie, lng, options.cookieMinutes, options.cookieDomain, options.cookieOptions); } } }; var querystring = { name: 'querystring', lookup: function lookup(options) { var found; if (typeof window !== 'undefined') { var query = window.location.search.substring(1); var params = query.split('&'); for (var i = 0; i < params.length; i++) { var pos = params[i].indexOf('='); if (pos > 0) { var key = params[i].substring(0, pos); if (key === options.lookupQuerystring) { found = params[i].substring(pos + 1); } } } } return found; } }; var hasLocalStorageSupport; try { hasLocalStorageSupport = window !== 'undefined' && window.localStorage !== null; var testKey = 'i18next.translate.boo'; window.localStorage.setItem(testKey, 'foo'); window.localStorage.removeItem(testKey); } catch (e) { hasLocalStorageSupport = false; } var localStorage$1 = { name: 'localStorage', lookup: function lookup(options) { var found; if (options.lookupLocalStorage && hasLocalStorageSupport) { var lng = window.localStorage.getItem(options.lookupLocalStorage); if (lng) found = lng; } return found; }, cacheUserLanguage: function cacheUserLanguage(lng, options) { if (options.lookupLocalStorage && hasLocalStorageSupport) { window.localStorage.setItem(options.lookupLocalStorage, lng); } } }; var hasSessionStorageSupport; try { hasSessionStorageSupport = window !== 'undefined' && window.sessionStorage !== null; var testKey$1 = 'i18next.translate.boo'; window.sessionStorage.setItem(testKey$1, 'foo'); window.sessionStorage.removeItem(testKey$1); } catch (e) { hasSessionStorageSupport = false; } var sessionStorage = { name: 'sessionStorage', lookup: function lookup(options) { var found; if (options.lookupsessionStorage && hasSessionStorageSupport) { var lng = window.sessionStorage.getItem(options.lookupsessionStorage); if (lng) found = lng; } return found; }, cacheUserLanguage: function cacheUserLanguage(lng, options) { if (options.lookupsessionStorage && hasSessionStorageSupport) { window.sessionStorage.setItem(options.lookupsessionStorage, lng); } } }; var navigator$1 = { name: 'navigator', lookup: function lookup(options) { var found = []; if (typeof navigator !== 'undefined') { if (navigator.languages) { // chrome only; not an array, so can't use .push.apply instead of iterating for (var i = 0; i < navigator.languages.length; i++) { found.push(navigator.languages[i]); } } if (navigator.userLanguage) { found.push(navigator.userLanguage); } if (navigator.language) { found.push(navigator.language); } } return found.length > 0 ? found : undefined; } }; var htmlTag = { name: 'htmlTag', lookup: function lookup(options) { var found; var htmlTag = options.htmlTag || (typeof document !== 'undefined' ? document.documentElement : null); if (htmlTag && typeof htmlTag.getAttribute === 'function') { found = htmlTag.getAttribute('lang'); } return found; } }; var path = { name: 'path', lookup: function lookup(options) { var found; if (typeof window !== 'undefined') { var language = window.location.pathname.match(/\/([a-zA-Z-]*)/g); if (language instanceof Array) { if (typeof options.lookupFromPathIndex === 'number') { if (typeof language[options.lookupFromPathIndex] !== 'string') { return undefined; } found = language[options.lookupFromPathIndex].replace('/', ''); } else { found = language[0].replace('/', ''); } } } return found; } }; var subdomain = { name: 'subdomain', lookup: function lookup(options) { var found; if (typeof window !== 'undefined') { var language = window.location.href.match(/(?:http[s]*\:\/\/)*(.*?)\.(?=[^\/]*\..{2,5})/gi); if (language instanceof Array) { if (typeof options.lookupFromSubdomainIndex === 'number') { found = language[options.lookupFromSubdomainIndex].replace('http://', '').replace('https://', '').replace('.', ''); } else { found = language[0].replace('http://', '').replace('https://', '').replace('.', ''); } } } return found; } }; function getDefaults() { return { order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'], lookupQuerystring: 'lng', lookupCookie: 'i18next', lookupLocalStorage: 'i18nextLng', // cache user language caches: ['localStorage'], excludeCacheFor: ['cimode'], //cookieMinutes: 10, //cookieDomain: 'myDomain' checkWhitelist: true, checkForSimilarInWhitelist: false }; } var Browser = /*#__PURE__*/ function () { function Browser(services) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, Browser); this.type = 'languageDetector'; this.detectors = {}; this.init(services, options); } _createClass(Browser, [{ key: "init", value: function init(services) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var i18nOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; this.services = services; this.options = defaults(options, this.options || {}, getDefaults()); // if checking for similar, user needs to check whitelist if (this.options.checkForSimilarInWhitelist) this.options.checkWhitelist = true; // backwards compatibility if (this.options.lookupFromUrlIndex) this.options.lookupFromPathIndex = this.options.lookupFromUrlIndex; this.i18nOptions = i18nOptions; this.addDetector(cookie$1); this.addDetector(querystring); this.addDetector(localStorage$1); this.addDetector(sessionStorage); this.addDetector(navigator$1); this.addDetector(htmlTag); this.addDetector(path); this.addDetector(subdomain); } }, { key: "addDetector", value: function addDetector(detector) { this.detectors[detector.name] = detector; } }, { key: "detect", value: function detect(detectionOrder) { var _this = this; if (!detectionOrder) detectionOrder = this.options.order; var detected = []; detectionOrder.forEach(function (detectorName) { if (_this.detectors[detectorName]) { var lookup = _this.detectors[detectorName].lookup(_this.options); if (lookup && typeof lookup === 'string') lookup = [lookup]; if (lookup) detected = detected.concat(lookup); } }); var found; detected.forEach(function (lng) { if (found) return; var cleanedLng = _this.services.languageUtils.formatLanguageCode(lng); if (!_this.options.checkWhitelist || _this.services.languageUtils.isWhitelisted(cleanedLng)) found = cleanedLng; if (!found && _this.options.checkForSimilarInWhitelist) { found = _this.getSimilarInWhitelist(cleanedLng); } }); if (!found) { var fallbacks = this.i18nOptions.fallbackLng; if (typeof fallbacks === 'string') fallbacks = [fallbacks]; if (!fallbacks) fallbacks = []; if (Object.prototype.toString.apply(fallbacks) === '[object Array]') { found = fallbacks[0]; } else { found = fallbacks[0] || fallbacks["default"] && fallbacks["default"][0]; } } return found; } }, { key: "cacheUserLanguage", value: function cacheUserLanguage(lng, caches) { var _this2 = this; if (!caches) caches = this.options.caches; if (!caches) return; if (this.options.excludeCacheFor && this.options.excludeCacheFor.indexOf(lng) > -1) return; caches.forEach(function (cacheName) { if (_this2.detectors[cacheName]) _this2.detectors[cacheName].cacheUserLanguage(lng, _this2.options); }); } }, { key: "getSimilarInWhitelist", value: function getSimilarInWhitelist(cleanedLng) { var _this3 = this; if (!this.i18nOptions.whitelist) return; if (cleanedLng.includes('-')) { // i.e. es-MX should check if es is in whitelist var prefix = cleanedLng.split('-')[0]; var cleanedPrefix = this.services.languageUtils.formatLanguageCode(prefix); if (this.services.languageUtils.isWhitelisted(cleanedPrefix)) return cleanedPrefix; // if reached here, nothing found. continue to search for similar using only prefix cleanedLng = cleanedPrefix; } // i.e. 'pt' should return 'pt-BR'. If multiple in whitelist with 'pt-', then use first one in whitelist var similar = this.i18nOptions.whitelist.find(function (whitelistLng) { var cleanedWhitelistLng = _this3.services.languageUtils.formatLanguageCode(whitelistLng); if (cleanedWhitelistLng.startsWith(cleanedLng)) return cleanedWhitelistLng; }); if (similar) return similar; } }]); return Browser; }(); Browser.type = 'languageDetector'; var fes = { autoplay: "The media that tried to play (with '{{src}}') wasn't allowed to by this browser, most likely due to the browser's autoplay policy.\n\n+ More info: {{url}}", checkUserDefinedFns: "It seems that you may have accidentally written {{name}} instead of {{actualName}}. Please correct it if it's not intentional.", fileLoadError: { bytes: "It looks like there was a problem loading your file. {{suggestion}}", font: "It looks like there was a problem loading your font. {{suggestion}}", gif: "There was some trouble loading your GIF. Make sure that your GIF is using 87a or 89a encoding.", image: "It looks like there was a problem loading your image. {{suggestion}}", json: "It looks like there was a problem loading your JSON file. {{suggestion}}", large: "If your large file isn't fetched successfully, we recommend splitting the file into smaller segments and fetching those.", strings: "It looks like there was a problem loading your text file. {{suggestion}}", suggestion: "Try checking if the file path ({{filePath}}) is correct, hosting the file online, or running a local server.\n\n+ More info: {{url}}", table: "It looks like there was a problem loading your table file. {{suggestion}}", xml: "It looks like there was a problem loading your XML file. {{suggestion}}" }, friendlyParamError: { type_EMPTY_VAR: "{{location}} {{func}}() was expecting {{formatType}} for the {{position}} parameter, received an empty variable instead. If not intentional, this is often a problem with scope.\n\n+ More info: {{url}}", type_TOO_FEW_ARGUMENTS: "{{location}} {{func}}() was expecting at least {{minParams}} arguments, but received only {{argCount}}.", type_TOO_MANY_ARGUMENTS: "{{location}} {{func}}() was expecting no more than {{maxParams}} arguments, but received {{argCount}}.", type_WRONG_TYPE: "{{location}} {{func}}() was expecting {{formatType}} for the {{position}} parameter, received {{argType}} instead." }, globalErrors: { reference: { cannotAccess: "\n{{location}} \"{{symbol}}\" is used before declaration. Make sure you have declared the variable before using it.\n\n+ More info: {{url}}", notDefined: "\n{{location}} \"{{symbol}}\" is not defined in the current scope. If you have defined it in your code, you should check its scope, spelling, and letter-casing (JavaScript is case-sensitive).\n\n+ More info: {{url}}" }, stackSubseq: "└[{{location}}] \n\t Called from line {{line}} in {{func}}()\n", stackTop: "┌[{{location}}] \n\t Error at line {{line}} in {{func}}()\n", syntax: { badReturnOrYield: "\nSyntax Error - return lies outside of a function. Make sure you’re not missing any brackets, so that return lies inside a function.\n\n+ More info: {{url}}", invalidToken: "\nSyntax Error - Found a symbol that JavaScript doesn't recognize or didn't expect at it's place.\n\n+ More info: {{url}}", missingInitializer: "\nSyntax Error - A const variable is declared but not initialized. In JavaScript, an initializer for a const is required. A value must be specified in the same statement in which the variable is declared. Check the line number in the error and assign the const variable a value.\n\n+ More info: {{url}}", redeclaredVariable: "\nSyntax Error - \"{{symbol}}\" is being redeclared. JavaScript doesn't allow declaring a variable more than once. Check the line number in error for redeclaration of the variable.\n\n+ More info: {{url}}", unexpectedToken: "\nSyntax Error - Symbol present at a place that wasn't expected.\nUsually this is due to a typo. Check the line number in the error for anything missing/extra.\n\n+ More info: {{url}}" }, type: { constAssign: "\n{{location}} A const variable is being re-assigned. In javascript, re-assigning a value to a constant is not allowed. If you want to re-assign new values to a variable, make sure it is declared as var or let.\n\n+ More info: {{url}}", notfunc: "\n{{location}} \"{{symbol}}\" could not be called as a function.\nCheck the spelling, letter-casing (JavaScript is case-sensitive) and its type.\n\n+ More info: {{url}}", notfuncObj: "\n{{location}} \"{{symbol}}\" could not be called as a function.\nVerify whether \"{{obj}}\" has \"{{symbol}}\" in it and check the spelling, letter-casing (JavaScript is case-sensitive) and its type.\n\n+ More info: {{url}}", readFromNull: "\n{{location}} The property of null can't be read. In javascript the value null indicates that an object has no value.\n\n+ More info: {{url}}", readFromUndefined: "\n{{location}} Cannot read property of undefined. Check the line number in error and make sure the variable which is being operated is not undefined.\n\n + More info: {{url}}" } }, libraryError: "{{location}} An error with message \"{{error}}\" occurred inside the p5js library when {{func}} was called. If not stated otherwise, it might be an issue with the arguments passed to {{func}}.", location: "[{{file}}, line {{line}}]", misspelling: "{{location}} It seems that you may have accidentally written \"{{name}}\" instead of \"{{actualName}}\". Please correct it to {{actualName}} if you wish to use the {{type}} from p5.js.", misspelling_plural: "{{location}} It seems that you may have accidentally written \"{{name}}\".\nYou may have meant one of the following: \n{{suggestions}}", misusedTopLevel: "Did you just try to use p5.js's {{symbolName}} {{symbolType}}? If so, you may want to move it into your sketch's setup() function.\n\n+ More info: {{url}}", preloadDisabled: "The preload() function has been removed in p5.js 2.0. Please load assets in setup() using async / await keywords or callbacks instead. See https://github.com/processing/p5.js-compatibility for more information about 2.0 and compatibility, or https://dev.to/limzykenneth/asynchronous-p5js-20-458f for more information about promises and async/await.", positions: { p_1: "first", p_10: "tenth", p_11: "eleventh", p_12: "twelfth", p_2: "second", p_3: "third", p_4: "fourth", p_5: "fifth", p_6: "sixth", p_7: "seventh", p_8: "eighth", p_9: "ninth" }, pre: "\n🌸 p5.js says: {{message}}", sketchReaderErrors: { reservedConst: "you have used a p5.js reserved variable \"{{symbol}}\" make sure you change the variable name to something else.\n\n+ More info: {{url}}", reservedFunc: "you have used a p5.js reserved function \"{{symbol}}\" make sure you change the function name to something else.\n\n+ More info: {{url}}" }, welcome: "Welcome! This is your friendly debugger. To turn me off, switch to using p5.min.js.", wrongPreload: "{{location}} An error with message \"{{error}}\" occurred inside the p5js library when \"{{func}}\" was called. If not stated otherwise, it might be due to \"{{func}}\" being called from preload. Nothing besides load calls (loadImage, loadJSON, loadFont, loadStrings, etc.) should be inside the preload function." }; var en = { fes: fes }; // Only one language is imported above. This is intentional as other languages // will be hosted online and then downloaded whenever needed /* * Here, we define a default/fallback language which we can use without internet. * You won't have to change this when adding a new language. * * `translation` is the namespace we are using for our initial set of strings */ var fallbackResources = { en: { translation: en } }; /* * This is a list of languages that we have added so far. * If you have just added a new language (yay!), add its key to the list below * (`en` is english, `es` es español). Also add its export to * dev.js, which is another file in this folder. * @private */ const languages = [ 'en', 'es', 'ko', 'zh', 'hi', 'ja' ]; if (typeof IS_MINIFIED === 'undefined') { // internationalization is only for the unminified build if (typeof P5_DEV_BUILD !== 'undefined') { // When the library is built in development mode ( using npm run dev ) // we want to use the current translation files on the disk, which may have // been updated but not yet pushed to the CDN. let completeResources = require('../../translations/dev'); for (const language of Object.keys(completeResources)) { // In es_translation, language is es and namespace is translation // In es_MX_translation, language is es-MX and namespace is translation const parts = language.split('_'); const lng = parts.slice(0, parts.length - 1).join('-'); const ns = parts[parts.length - 1]; fallbackResources[lng] = fallbackResources[lng] || {}; fallbackResources[lng][ns] = completeResources[language]; } } } /* * This is our i18next "backend" plugin. It tries to fetch languages * from a CDN. * @private */ class FetchResources { constructor(services, options) { this.init(services, options); } // run fetch with a timeout. Automatically rejects on timeout // default timeout = 2000 ms fetchWithTimeout(url, options, timeout = 2000) { return Promise.race([ fetch(url, options), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout) ) ]); } init(services, options = {}) { this.services = services; this.options = options; } read(language, namespace, callback) { const loadPath = this.options.loadPath; if (language === this.options.fallback) { // if the default language of the user is the same as our inbuilt fallback, // there's no need to fetch resources from the cdn. This won't actually // need to run when we use "partialBundledLanguages" in the init // function. callback(null, fallbackResources[language][namespace]); } else if (languages.includes(language)) { // The user's language is included in the list of languages // that we so far added translations for. const url = this.services.interpolator.interpolate(loadPath, { lng: language, ns: namespace }); this.loadUrl(url, callback); } else { // We don't have translations for this language. i18next will use // the default language instead. callback('Not found', false); } } loadUrl(url, callback) { this.fetchWithTimeout(url) .then( response => { const ok = response.ok; if (!ok) { // caught in the catch() below throw new Error(`failed loading ${url}`); } return response.json(); }, () => { // caught in the catch() below throw new Error(`failed loading ${url}`); } ) .then(data => { return callback(null, data); }) .catch(callback); } } FetchResources.type = 'backend'; /** * This is our translation function. Give it a key and * it will retrieve the appropriate string * (within supported languages) according to the * user's browser's language settings. * @function translator * @param {String} key a key that corresponds to a message in our translation files * @param {Object} values values for use in the message under the given `key` * @returns {String} message (with values inserted) in the user's browser language * @private */ let translator = (key, values) => { console.debug('p5.js translator called before translations were loaded'); // Certain FES functionality may trigger before translations are downloaded. // Using "partialBundledLanguages" option during initialization, we can // still use our fallback language to display messages i18next.t(key, values); /* i18next-extract-disable-line */ }; // (We'll set this to a real value in the init function below!) /* * Set up our translation function, with loaded languages * @private */ const initialize = () => { let i18init = i18next .use(Browser) .use(FetchResources) .init({ fallbackLng: 'en', nestingPrefix: '$tr(', nestingSuffix: ')', defaultNS: 'translation', returnEmptyString: false, interpolation: { escapeValue: false }, detection: { checkWhitelist: false, // prevent storing or locating language from cookie or localStorage // more info on https://github.com/processing/p5.js/issues/4862 order: ['querystring', 'navigator', 'htmlTag', 'path', 'subdomain'], caches: [] }, backend: { fallback: 'en', // ensure that the FES internationalization strings are loaded // from the latest patch of the current minor version of p5.js loadPath: `https://cdn.jsdelivr.net/npm/p5@${ VERSION.replace(/^(\d+\.\d+)\.\d+.*$/, '$1') }/translations/{{lng}}/{{ns}}.json` }, partialBundledLanguages: true, resources: fallbackResources }) .then( translateFn => { translator = translateFn; }, e => console.debug(`Translations failed to load (${e})`) ); // i18next.init() returns a promise that resolves when the translations // are loaded. We use this in core/init.js to hold p5 initialization until // we have the translation files. return i18init; }; // This contains a data table used by ./fes_core.js/fesErrorMonitor(). // // Note: Different browsers use different error strings for the same error. // Extracting info from the browser error messages is easier and cleaner // if we have a predefined lookup. This file serves as that lookup. // Using this lookup we match the errors obtained from the browser, classify // them into types and extract the required information. // The FES can use the extracted info to generate a friendly error message // for the matching error. const strings$1 = { ReferenceError: [ { msg: '{{}} is not defined', type: 'NOTDEFINED', browser: 'all' }, { msg: "Can't find variable: {{}}", type: 'NOTDEFINED', browser: 'Safari' }, { msg: "Cannot access '{{.}}' before initialization", type: 'CANNOTACCESS', browser: 'Chrome' }, { msg: "can't access lexical declaration '{{.}}' before initialization", type: 'CANNOTACCESS', browser: 'Firefox' } ], SyntaxError: [ { msg: 'illegal character', type: 'INVALIDTOKEN', browser: 'Firefox' }, { msg: 'Invalid character', type: 'INVALIDTOKEN', browser: 'Safari' }, { msg: 'Invalid or unexpected token', type: 'INVALIDTOKEN', browser: 'Chrome' }, { msg: "Unexpected token '{{.}}'", type: 'UNEXPECTEDTOKEN', browser: 'Chrome' }, { msg: "expected {{.}}, got '{{.}}'", type: 'UNEXPECTEDTOKEN', browser: 'Chrome' }, { msg: "Identifier '{{.}}' has already been declared", type: 'REDECLAREDVARIABLE', browser: 'Chrome' }, { msg: 'redeclaration of {} {{.}}', type: 'REDECLAREDVARIABLE', browser: 'Firefox' }, { msg: 'Missing initializer in const declaration', type: 'MISSINGINITIALIZER', browser: 'Chrome' }, { msg: 'missing = in const declaration', type: 'MISSINGINITIALIZER', browser: 'Firefox' }, { msg: 'Illegal return statement', type: 'BADRETURNORYIELD', browser: 'Chrome' }, { msg: 'return not in function', type: 'BADRETURNORYIELD', browser: 'Firefox' } ], TypeError: [ { msg: '{{.}} is not a function', type: 'NOTFUNC', browser: 'all' }, { msg: 'Cannot read {{.}} null', type: 'READNULL', browser: 'Chrome' }, { msg: '{{.}} is null', type: 'READNULL', browser: 'Firefox' }, { msg: 'Cannot read {{.}} undefined', type: 'READUDEFINED', browser: 'Chrome' }, { msg: '{{.}} is undefined', type: 'READUDEFINED', browser: 'Firefox' }, { msg: 'Assignment to constant variable', type: 'CONSTASSIGN', browser: 'Chrome' }, { msg: "invalid assignment to const '{{.}}'", type: 'CONSTASSIGN', browser: 'Firefox' } ] }; /** * @for p5 * @requires core * * This is the main file for the Friendly Error System (FES), containing * the core as well as miscellaneous functionality of the FES. Here is a * brief outline of the functions called in this system. * * The FES may be invoked by a call to either * (1) _validateParameters, (2) _friendlyFileLoadError, (3) _friendlyError, * (4) helpForMisusedAtTopLevelCode, or (5) _fesErrorMonitor. * * _validateParameters is located in validate_params.js along with other code * used for parameter validation. * _friendlyFileLoadError is located in file_errors.js along with other code * used for dealing with file load errors. * Apart from this, there's also a file stacktrace.js, which contains the code * to parse the error stack, borrowed from: * https://github.com/stacktracejs/stacktrace.js * * For more detailed information on the FES functions, including the call * sequence of each function, please look at the FES Reference + Dev Notes: * https://github.com/processing/p5.js/blob/main/contributor_docs/fes_reference_dev_notes.md */ function fesCore(p5, fn){ // p5.js blue, p5.js orange, auto dark green; fallback p5.js darkened magenta // See testColors below for all the color codes and names const typeColors = ['#2D7BB6', '#EE9900', '#4DB200', '#C83C00']; let misusedAtTopLevelCode = null; let defineMisusedAtTopLevelCode = null; // the threshold for the maximum allowed levenshtein distance // used in misspelling detection const EDIT_DIST_THRESHOLD = 2; // Used for internally thrown errors that should not get wrapped by another // friendly error handler class FESError extends Error {} if (typeof IS_MINIFIED !== 'undefined') { p5._friendlyError = p5._checkForUserDefinedFunctions = p5._fesErrorMonitor = () => {}; } else { // const errorTable = require('./browser_errors').default; // -- Borrowed from jQuery 1.11.3 -- const class2type = {}; const toString = class2type.toString; const names = [ 'Boolean', 'Number', 'String', 'Function', 'Array', 'Date', 'RegExp', 'Object', 'Error' ]; for (let n = 0; n < names.length; n++) { class2type[`[object ${names[n]}]`] = names[n].toLowerCase(); } const getType = obj => { if (obj == null) { return `${obj}`; } return typeof obj === 'object' || typeof obj === 'function' ? class2type[toString.call(obj)] || 'object' : typeof obj; }; // -- End borrow -- // entry points into user-defined code const entryPoints = [ 'setup', 'draw', 'deviceMoved', 'deviceTurned', 'deviceShaken', 'doubleClicked', 'mousePressed', 'mouseReleased', 'mouseMoved', 'mouseDragged', 'mouseClicked', 'mouseWheel', 'touchStarted', 'touchMoved', 'touchEnded', 'keyPressed', 'keyReleased', 'keyTyped', 'windowResized' ]; /** * Takes a message and a p5 function func, and adds a link pointing to * the reference documentation of func at the end of the message * * @method mapToReference * @private * @param {String} message the words to be said * @param {String} [func] the name of function * * @returns {String} */ const mapToReference = (message, func) => { let msgWithReference = ''; if (func == null || func.substring(0, 4) === 'load') { msgWithReference = message; } else { const methodParts = func.split('.'); const referenceSection = methodParts.length > 1 ? `${methodParts[0]}.${methodParts[1]}` : 'p5'; const funcName = methodParts.length === 1 ? func : methodParts.slice(2).join('/'); //Whenever func having p5.[Class] is encountered, we need to have the error link as mentioned below else different link funcName.startsWith('p5.') ? msgWithReference = `${message} (https://p5js.org/reference/${referenceSection}.${funcName})` : msgWithReference = `${message} (https://p5js.org/reference/${referenceSection}/${funcName})`; } return msgWithReference; }; /** * Prints out a fancy, colorful message to the console log * Attaches Friendly Errors prefix [fes.pre] to the message. * * @method _report * @private * @param {String} message Message to be printed * @param {String} [func] Name of function * @param {Number|String} [color] CSS color code * * @return console logs */ p5._report = (message, func, color) => { // if p5._fesLogger is set ( i.e we are running tests ), use that // instead of console.log const log = p5._fesLogger == null ? console.log.bind(console) : p5._fesLogger; if ('undefined' === getType(color)) { color = '#B40033'; // dark magenta } else if (getType(color) === 'number') { // Type to color color = typeColors[color]; } // Add a link to the reference docs of func at the end of the message message = mapToReference(message, func); const prefixedMsg = translator('fes.pre', { message }); { log(prefixedMsg); } }; /** * Throws an error with helpful p5 context. Similar to _report, but * this will stop other code execution to prevent downstream errors * from being logged. * * @method _error * @private * @param context p5 instance the error is from * @param {String} message Message to be printed * @param {String} [func] Name of function */ p5._error = (context, message, func) => { p5._report(message, func); context.hitCriticalError = true; // Throw an error to stop the current function (e.g. setup or draw) from // running more code throw new FESError('Stopping sketch to prevent more errors'); }; /** * This is a generic method that can be called from anywhere in the p5 * library to alert users to a common error. * * @method _friendlyError * @private * @param {String} message Message to be printed * @param {String} [func] Name of the function linked to error * @param {Number|String} [color] CSS color code */ p5._friendlyError = function(message, func, color) { if (p5.disableFriendlyErrors) return; p5._report(message, func, color); }; /** * This is called internally if there is an error with autoplay. Generates * and prints a friendly error message [fes.autoplay]. * * @method _friendlyAutoplayError * @private */ p5._friendlyAutoplayError = function(src) { const message = translator('fes.autoplay', { src, url: 'https://developer.mozilla.org/docs/Web/Media/Autoplay_guide' }); console.log(translator('fes.pre', { message })); }; /** * Measures dissimilarity between two strings by calculating * the Levenshtein distance. * * If the "distance" between them is small enough, it is * reasonable to think that one is the misspelled version of the other. * * Specifically, this uses the Wagner–Fischer algorithm. * @method computeEditDistance * @private * @param {String} w1 the first word * @param {String} w2 the second word * * @returns {Number} the "distance" between the two words, a smaller value * indicates that the words are similar */ const computeEditDistance = (w1, w2) => { const l1 = w1.length, l2 = w2.length; if (l1 === 0) return w2; if (l2 === 0) return w1; let prev = []; let cur = []; for (let j = 0; j < l2 + 1; j++) { cur[j] = j; } prev = cur; for (let i = 1; i < l1 + 1; i++) { cur = []; for (let j = 0; j < l2 + 1; j++) { if (j === 0) { cur[j] = i; } else { let a1 = w1[i - 1], a2 = w2[j - 1]; let temp = 999999; let cost = a1.toLowerCase() === a2.toLowerCase() ? 0 : 1; temp = temp > cost + prev[j - 1] ? cost + prev[j - 1] : temp; temp = temp > 1 + cur[j - 1] ? 1 + cur[j - 1] : temp; temp = temp > 1 + prev[j] ? 1 + prev[j] : temp; cur[j] = temp; } } prev = cur; } return cur[l2]; }; /** * Whether or not p5.js is running in an environment where `preload` will be * run before `setup`. * * This will return false for default builds >= 2.0, but backwards compatibility * addons may set this to true. * * @private */ p5.isPreloadSupported = function() { return false; }; /** * Checks capitalization for user defined functions. * * Generates and prints a friendly error message using key: * "fes.checkUserDefinedFns". * * @method checkForUserDefinedFunctions * @private * @param {*} context Current default context. Set to window in * "global mode" and to a p5 instance in "instance mode" */ const checkForUserDefinedFunctions = context => { if (p5.disableFriendlyErrors) return; // if using instance mode, this function would be called with the current // instance as context const instanceMode = context instanceof p5; context = instanceMode ? context : window; const fnNames = entryPoints; if (context.preload && !p5.isPreloadSupported()) { p5._error(context, translator('fes.preloadDisabled')); } const fxns = {}; // lowercasename -> actualName mapping fnNames.forEach(symbol => { fxns[symbol.toLowerCase()] = symbol; }); for (const prop of Object.keys(context)) { const lowercase = prop.toLowerCase(); // check if the lowercase property name has an entry in fxns, if the // actual name with correct capitalization doesnt exist in context, // and if the user-defined symbol is of the type function if ( fxns.hasOwnProperty(lowercase) && !context[fxns[lowercase]] && typeof context[prop] === 'function' ) { const msg = translator('fes.checkUserDefinedFns', { name: prop, actualName: fxns[lowercase] }); p5._friendlyError(msg, fxns[lowercase]); } } }; /** * Compares the symbol caught in the ReferenceError to everything in * misusedAtTopLevel ( all public p5 properties ). * * Generates and prints a friendly error message using key: "fes.misspelling". * * @method handleMisspelling * @private * @param {String} errSym Symbol to whose spelling to check * @param {Error} error ReferenceError object * * @returns {Boolean} tell whether error was likely due to typo */ const handleMisspelling = (errSym, error) => { if (!misusedAtTopLevelCode) { defineMisusedAtTopLevelCode(); } const distanceMap = {}; let min = 999999; // compute the levenshtein distance for the symbol against all known // public p5 properties. Find the property with the minimum distance misusedAtTopLevelCode.forEach(symbol => { let dist = computeEditDistance(errSym, symbol.name); if (distanceMap[dist]) distanceMap[dist].push(symbol); else distanceMap[dist] = [symbol]; if (dist < min) min = dist; }); // if the closest match has more "distance" than the max allowed threshold if (min > Math.min(EDIT_DIST_THRESHOLD, errSym.length)) return false; // Show a message only if the caught symbol and the matched property name // differ in their name ( either letter difference or difference of case ) const matchedSymbols = distanceMap[min].filter( symbol => symbol.name !== errSym ); if (matchedSymbols.length !== 0) { const parsed = p5._getErrorStackParser().parse(error); let locationObj; if ( parsed && parsed[0] && parsed[0].fileName && parsed[0].lineNumber && parsed[0].columnNumber ) { locationObj = { location: `${parsed[0].fileName}:${parsed[0].lineNumber}:${ parsed[0].columnNumber }`, file: parsed[0].fileName.split('/').slice(-1), line: parsed[0].lineNumber }; } let msg; if (matchedSymbols.length === 1) { // To be used when there is only one closest match. The count parameter // allows i18n to pick between the keys "fes.misspelling" and // "fes.misspelling_plural" msg = translator('fes.misspelling', { name: errSym, actualName: matchedSymbols[0].name, type: matchedSymbols[0].type, location: locationObj ? translator('fes.location', locationObj) : '', count: matchedSymbols.length }); } else { // To be used when there are multiple closest matches. Gives each // suggestion on its own line, the function name followed by a link to // reference documentation const suggestions = matchedSymbols .map(symbol => { const message = '▶️ ' + symbol.name + (symbol.type === 'function' ? '()' : ''); return mapToReference(message, symbol.name); }) .join('\n'); msg = translator('fes.misspelling', { name: errSym, suggestions, location: locationObj ? translator('fes.location', locationObj) : '', count: matchedSymbols.length }); } // If there is only one closest match, tell _friendlyError to also add // a link to the reference documentation. In case of multiple matches, // this is already done in the suggestions variable, one link for each // suggestion. p5._friendlyError( msg, matchedSymbols.length === 1 ? matchedSymbols[0].name : undefined ); return true; } return false; }; /** * Prints a friendly stacktrace for user-written functions for "global" errors * * Generates and prints a friendly error message using key: * "fes.globalErrors.stackTop", "fes.globalErrors.stackSubseq". * * @method printFriendlyStack * @private * @param {Array} friendlyStack */ const printFriendlyStack = friendlyStack => { const log = p5._fesLogger && typeof p5._fesLogger === 'function' ? p5._fesLogger : console.log.bind(console); if (friendlyStack.length > 1) { let stacktraceMsg = ''; friendlyStack.forEach((frame, idx) => { const location = `${frame.fileName}:${frame.lineNumber}:${ frame.columnNumber }`; let frameMsg, translationObj = { func: frame.functionName, line: frame.lineNumber, location, file: frame.fileName.split('/').slice(-1) }; if (idx === 0) { frameMsg = translator('fes.globalErrors.stackTop', translationObj); } else { frameMsg = translator('fes.globalErrors.stackSubseq', translationObj); } stacktraceMsg += frameMsg; }); log(stacktraceMsg); } }; /** * Takes a stacktrace array and filters out all frames that show internal p5 * details. * * Generates and prints a friendly error message using key: * "fes.wrongPreload", "fes.libraryError". * * The processed stack is used to find whether the error happened internally * within the library, and if the error was due to a non-loadX() method * being used in preload. * * "Internally" here means that the exact location of the error (the top of * the stack) is a piece of code written in the p5.js library (which may or * may not have been called from the user's sketch). * * @method processStack * @private * @param {Error} error * @param {Array} stacktrace * * @returns {Array} An array with two elements, [isInternal, friendlyStack] * isInternal: a boolean value indicating whether the error * happened internally * friendlyStack: the filtered (simplified) stacktrace */ const processStack = (error, stacktrace) => { // cannot process a stacktrace that doesn't exist if (!stacktrace) return [false, null]; stacktrace.forEach(frame => { frame.functionName = frame.functionName || ''; }); // isInternal - Did this error happen inside the library let isInternal = false; let p5FileName, friendlyStack, currentEntryPoint; // Intentionally throw an error that we catch so that we can check the name // of the current file. Any errors we see from this file, we treat as // internal errors. try { throw new Error(); } catch (testError) { const testStacktrace = p5._getErrorStackParser().parse(testError); p5FileName = testStacktrace[0].fileName; } for (let i = stacktrace.length - 1; i >= 0; i--) { let splitted = stacktrace[i].functionName.split('.'); if (entryPoints.includes(splitted[splitted.length - 1])) { // remove everything below an entry point function (setup, draw, etc). // (it's usually the internal initialization calls) friendlyStack = stacktrace.slice(0, i + 1); currentEntryPoint = splitted[splitted.length - 1]; // We call the error "internal" if the source of the error was a // function from within the p5.js library file, but called from the // user's code directly. We only need to check the topmost frame in // the stack trace since any function internal to p5 should pass this // check, not just public p5 functions. if (stacktrace[0].fileName === p5FileName) { isInternal = true; break; } break; } } // in some cases ( errors in promises, callbacks, etc), no entry-point // function may be found in the stacktrace. In that case just use the // entire stacktrace for friendlyStack if (!friendlyStack) friendlyStack = stacktrace; if (isInternal) { // the frameIndex property is added before the filter, so frameIndex // corresponds to the index of a frame in the original stacktrace. // Then we filter out all frames which belong to the file that contains // the p5 library friendlyStack = friendlyStack .map((frame, index) => { frame.frameIndex = index; return frame; }) .filter(frame => frame.fileName !== p5FileName); // a weird case, if for some reason we can't identify the function called // from user's code if (friendlyStack.length === 0) return [true, null]; // get the function just above the topmost frame in the friendlyStack. // i.e the name of the library function called from user's code const func = stacktrace[friendlyStack[0].frameIndex - 1].functionName .split('.') .slice(-1)[0]; // Try and get the location (line no.) from the top element of the stack let locationObj; if ( friendlyStack[0].fileName && friendlyStack[0].lineNumber && friendlyStack[0].columnNumber ) { locationObj = { location: `${friendlyStack[0].fileName}:${ friendlyStack[0].lineNumber }:${friendlyStack[0].columnNumber}`, file: friendlyStack[0].fileName.split('/').slice(-1), line: friendlyStack[0].lineNumber }; // if already handled by another part of the FES, don't handle again if (p5._fesLogCache[locationObj.location]) return [true, null]; } // Check if the error is due to a non loadX method being used incorrectly // in preload if ( currentEntryPoint === 'preload' && fn._preloadMethods[func] == null ) { p5._friendlyError( translator('fes.wrongPreload', { func, location: locationObj ? translator('fes.location', locationObj) : '', error: error.message }), 'preload' ); } else { // Library error p5._friendlyError( translator('fes.libraryError', { func, location: locationObj ? translator('fes.location', locationObj) : '', error: error.message }), func ); } // Finally, if it's an internal error, print the friendlyStack // ( fesErrorMonitor won't handle this error ) if (friendlyStack && friendlyStack.length) { printFriendlyStack(friendlyStack); } } return [isInternal, friendlyStack]; }; /** * Handles "global" errors that the browser catches. * * Called when an error event happens and detects the type of error. * * Generates and prints a friendly error message using key: * "fes.globalErrors.syntax.[*]", "fes.globalErrors.reference.[*]", * "fes.globalErrors.type.[*]". * * @method fesErrorMonitor * @private * @param {*} e Event object to extract error details from */ const fesErrorMonitor = e => { if (p5.disableFriendlyErrors) return; // Don't try to handle an error intentionally emitted by FES to halt execution if (e && (e instanceof FESError || e.reason instanceof FESError)) return; // Try to get the error object from e let error; if (e instanceof Error) { error = e; } else if (e instanceof ErrorEvent) { error = e.error; } else if (e instanceof PromiseRejectionEvent) { error = e.reason; if (!(error instanceof Error)) return; } if (!error) return; let stacktrace = p5._getErrorStackParser().parse(error); // process the stacktrace from the browser and simplify it to give // friendlyStack. let [isInternal, friendlyStack] = processStack(error, stacktrace); // if this is an internal library error, the type of the error is not relevant, // only the user code that lead to it is. if (isInternal) { return; } const errList = strings$1[error.name]; if (!errList) return; // this type of error can't be handled yet let matchedError; for (const obj of errList) { let string = obj.msg; // capture the primary symbol mentioned in the error string = string.replace(new RegExp('{{}}', 'g'), '([a-zA-Z0-9_]+)'); string = string.replace(new RegExp('{{.}}', 'g'), '(.+)'); string = string.replace(new RegExp('{}', 'g'), '(?:[a-zA-Z0-9_]+)'); let matched = error.message.match(string); if (matched) { matchedError = Object.assign({}, obj); matchedError.match = matched; break; } } if (!matchedError) return; // Try and get the location from the top element of the stack let locationObj; if ( stacktrace && stacktrace[0].fileName && stacktrace[0].lineNumber && stacktrace[0].columnNumber ) { locationObj = { location: `${stacktrace[0].fileName}:${stacktrace[0].lineNumber}:${ stacktrace[0].columnNumber }`, file: stacktrace[0].fileName.split('/').slice(-1), line: friendlyStack[0].lineNumber }; } switch (error.name) { case 'SyntaxError': { // We can't really do much with syntax errors other than try to use // a simpler framing of the error message. The stack isn't available // for syntax errors switch (matchedError.type) { case 'INVALIDTOKEN': { //Error if there is an invalid or unexpected token that doesn't belong at this position in the code //let x = “not a string”; -> string not in proper quotes let url = 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Illegal_character#What_went_wrong'; p5._friendlyError( translator('fes.globalErrors.syntax.invalidToken', { url }) ); break; } case 'UNEXPECTEDTOKEN': { //Error if a specific language construct(, { ; etc) was expected, but something else was provided //for (let i = 0; i < 5,; ++i) -> a comma after i<5 instead of a semicolon let url = 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong'; p5._friendlyError( translator('fes.globalErrors.syntax.unexpectedToken', { url }) ); break; } case 'REDECLAREDVARIABLE': { //Error if a variable is redeclared by the user. Example=> //let a = 10; //let a = 100; let errSym = matchedError.match[1]; let url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Redeclared_parameter#what_went_wrong'; p5._friendlyError( translator('fes.globalErrors.syntax.redeclaredVariable', { symbol: errSym, url }) ); break; } case 'MISSINGINITIALIZER': { //Error if a const variable is not initialized during declaration //Example => const a; let url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Missing_initializer_in_const#what_went_wrong'; p5._friendlyError( translator('fes.globalErrors.syntax.missingInitializer', { url }) ); break; } case 'BADRETURNORYIELD': { //Error when a return statement is misplaced(usually outside of a function) // const a = function(){ // ..... // } // return; -> misplaced return statement let url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Bad_return_or_yield#what_went_wrong'; p5._friendlyError( translator('fes.globalErrors.syntax.badReturnOrYield', { url }) ); break; } } break; } case 'ReferenceError': { switch (matchedError.type) { case 'NOTDEFINED': { //Error if there is a non-existent variable referenced somewhere //let a = 10; //console.log(x); let errSym = matchedError.match[1]; if (errSym && handleMisspelling(errSym, error)) { break; } // if the flow gets this far, this is likely not a misspelling // of a p5 property/function let url = 'https://p5js.org/examples/data-variable-scope.html'; p5._friendlyError( translator('fes.globalErrors.reference.notDefined', { url, symbol: errSym, location: locationObj ? translator('fes.location', locationObj) : '' }) ); if (friendlyStack) printFriendlyStack(friendlyStack); break; } case 'CANNOTACCESS': { //Error if a lexical variable was accessed before it was initialized //console.log(a); -> variable accessed before it was initialized //let a=100; let errSym = matchedError.match[1]; let url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_lexical_declaration_before_init#what_went_wrong'; p5._friendlyError( translator('fes.globalErrors.reference.cannotAccess', { url, symbol: errSym, location: locationObj ? translator('fes.location', locationObj) : '' }) ); if (friendlyStack) printFriendlyStack(friendlyStack); break; } } break; } case 'TypeError': { switch (matchedError.type) { case 'NOTFUNC': { //Error when some code expects you to provide a function, but that didn't happen //let a = document.getElementByID('foo'); -> getElementById instead of getElementByID let errSym = matchedError.match[1]; let splitSym = errSym.split('.'); let url = 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_a_function#What_went_wrong'; // if errSym is aa.bb.cc , symbol would be cc and obj would aa.bb let translationObj = { url, symbol: splitSym[splitSym.length - 1], obj: splitSym.slice(0, splitSym.length - 1).join('.'), location: locationObj ? translator('fes.location', locationObj) : '' }; // There are two cases to handle here. When the function is called // as a property of an object and when it's called independently. // Both have different explanations. if (splitSym.length > 1) { p5._friendlyError( translator('fes.globalErrors.type.notfuncObj', translationObj) ); } else { p5._friendlyError( translator('fes.globalErrors.type.notfunc', translationObj) ); } if (friendlyStack) printFriendlyStack(friendlyStack); break; } case 'READNULL': { //Error if a property of null is accessed //let a = null; //console.log(a.property); -> a is null let errSym = matchedError.match[1]; let url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_property#what_went_wrong'; /*let url2 = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null';*/ p5._friendlyError( translator('fes.globalErrors.type.readFromNull', { url, symbol: errSym, location: locationObj ? translator('fes.location', locationObj) : '' }) ); if (friendlyStack) printFriendlyStack(friendlyStack); break; } case 'READUDEFINED': { //Error if a property of undefined is accessed //let a; -> default value of a is undefined //console.log(a.property); -> a is undefined let errSym = matchedError.match[1]; let url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_property#what_went_wrong'; /*let url2 = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined#description';*/ p5._friendlyError( translator('fes.globalErrors.type.readFromUndefined', { url, symbol: errSym, location: locationObj ? translator('fes.location', locationObj) : '' }) ); if (friendlyStack) printFriendlyStack(friendlyStack); break; } case 'CONSTASSIGN': { //Error when a const variable is reassigned a value //const a = 100; //a=10; let url = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_const_assignment#what_went_wrong'; p5._friendlyError( translator('fes.globalErrors.type.constAssign', { url, location: locationObj ? translator('fes.location', locationObj) : '' }) ); if (friendlyStack) printFriendlyStack(friendlyStack); break; } } } } }; p5._fesErrorMonitor = fesErrorMonitor; p5._checkForUserDefinedFunctions = checkForUserDefinedFunctions; // logger for testing purposes. p5._fesLogger = null; p5._fesLogCache = {}; window.addEventListener('load', checkForUserDefinedFunctions, false); window.addEventListener('error', p5._fesErrorMonitor, false); window.addEventListener('unhandledrejection', p5._fesErrorMonitor, false); /** * Prints out all the colors in the color pallete with white text. * For color blindness testing. */ /* function testColors() { const str = 'A box of biscuits, a box of mixed biscuits and a biscuit mixer'; p5._friendlyError(str, 'print', '#ED225D'); // p5.js magenta p5._friendlyError(str, 'print', '#2D7BB6'); // p5.js blue p5._friendlyError(str, 'print', '#EE9900'); // p5.js orange p5._friendlyError(str, 'print', '#A67F59'); // p5.js light brown p5._friendlyError(str, 'print', '#704F21'); // p5.js gold p5._friendlyError(str, 'print', '#1CC581'); // auto cyan p5._friendlyError(str, 'print', '#FF6625'); // auto orange p5._friendlyError(str, 'print', '#79EB22'); // auto green p5._friendlyError(str, 'print', '#B40033'); // p5.js darkened magenta p5._friendlyError(str, 'print', '#084B7F'); // p5.js darkened blue p5._friendlyError(str, 'print', '#945F00'); // p5.js darkened orange p5._friendlyError(str, 'print', '#6B441D'); // p5.js darkened brown p5._friendlyError(str, 'print', '#2E1B00'); // p5.js darkened gold p5._friendlyError(str, 'print', '#008851'); // auto dark cyan p5._friendlyError(str, 'print', '#C83C00'); // auto dark orange p5._friendlyError(str, 'print', '#4DB200'); // auto dark green } */ } // This is a lazily-defined list of p5 symbols that may be // misused by beginners at top-level code, outside of setup/draw. We'd like // to detect these errors and help the user by suggesting they move them // into setup/draw. // // For more details, see https://github.com/processing/p5.js/issues/1121. misusedAtTopLevelCode = null; const FAQ_URL = 'https://github.com/processing/p5.js/wiki/p5.js-overview#why-cant-i-assign-variables-using-p5-functions-and-variables-before-setup'; /** * A helper function for populating misusedAtTopLevel list. * * @method defineMisusedAtTopLevelCode * @private */ defineMisusedAtTopLevelCode = () => { const uniqueNamesFound = {}; const getSymbols = obj => Object.getOwnPropertyNames(obj) .filter(name => { if (name[0] === '_') { return false; } if (name in uniqueNamesFound) { return false; } uniqueNamesFound[name] = true; return true; }) .map(name => { let type; if (typeof obj[name] === 'function') { type = 'function'; } else if (name === name.toUpperCase()) { type = 'constant'; } else { type = 'variable'; } return { name, type }; }); misusedAtTopLevelCode = [].concat( getSymbols(fn), // At present, p5 only adds its constants to fn during // construction, which may not have happened at the time a // ReferenceError is thrown, so we'll manually add them to our list. getSymbols(constants) ); // This will ultimately ensure that we report the most specific error // possible to the user, e.g. advising them about HALF_PI instead of PI // when their code misuses the former. misusedAtTopLevelCode.sort((a, b) => b.name.length - a.name.length); }; /** * Detects browser level error event for p5 constants/functions used outside * of setup() and draw(). * * Generates and prints a friendly error message using key: * "fes.misusedTopLevel". * * @method helpForMisusedAtTopLevelCode * @private * @param {Event} e Error event * @param {Boolean} log false * * @returns {Boolean} true */ const helpForMisusedAtTopLevelCode = (e, log) => { if (!log) { log = console.log.bind(console); } if (!misusedAtTopLevelCode) { defineMisusedAtTopLevelCode(); } // If we find that we're logging lots of false positives, we can // uncomment the following code to avoid displaying anything if the // user's code isn't likely to be using p5's global mode. (Note that // setup/draw are more likely to be defined due to JS function hoisting.) // //if (!('setup' in window || 'draw' in window)) { // return; //} misusedAtTopLevelCode.some(symbol => { // Note that while just checking for the occurrence of the // symbol name in the error message could result in false positives, // a more rigorous test is difficult because different browsers // log different messages, and the format of those messages may // change over time. // // For example, if the user uses 'PI' in their code, it may result // in any one of the following messages: // // * 'PI' is undefined (Microsoft Edge) // * ReferenceError: PI is undefined (Firefox) // * Uncaught ReferenceError: PI is not defined (Chrome) if (e.message && e.message.match(`\\W?${symbol.name}\\W`) !== null) { const symbolName = symbol.type === 'function' ? `${symbol.name}()` : symbol.name; if (typeof IS_MINIFIED !== 'undefined') { log( `Did you just try to use p5.js's ${symbolName} ${ symbol.type }? If so, you may want to move it into your sketch's setup() function.\n\nFor more details, see: ${FAQ_URL}` ); } else { log( translator('fes.misusedTopLevel', { symbolName, symbolType: symbol.type, url: FAQ_URL }) ); } return true; } }); }; // Exposing this primarily for unit testing. fn._helpForMisusedAtTopLevelCode = helpForMisusedAtTopLevelCode; if (document.readyState !== 'complete') { window.addEventListener('error', helpForMisusedAtTopLevelCode, false); // Our job is only to catch ReferenceErrors that are thrown when // global (non-instance mode) p5 APIs are used at the top-level // scope of a file, so we'll unbind our error listener now to make // sure we don't log false positives later. window.addEventListener('load', () => { window.removeEventListener('error', helpForMisusedAtTopLevelCode, false); }); } } if (typeof p5 !== 'undefined') { fesCore(p5, p5.prototype); } /** * @for p5 * @requires core */ // Borrow from stacktracejs https://github.com/stacktracejs/stacktrace.js with // minor modifications. The license for the same and the code is included below // Copyright (c) 2017 Eric Wendelin and other contributors // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. function ErrorStackParser() { let FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; let CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; let SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; return { /** * Given an Error object, extract the most information from it. * @private * @param {Error} error object * @return {Array} of stack frames */ parse: function ErrorStackParser$$parse(error) { if ( typeof error.stacktrace !== 'undefined' || typeof error['opera#sourceloc'] !== 'undefined' ) { return this.parseOpera(error); } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { return this.parseV8OrIE(error); } else if (error.stack) { return this.parseFFOrSafari(error); } else ; }, // Separate line and column numbers from a string of the form: (URI:Line:Column) extractLocation: function ErrorStackParser$$extractLocation(urlLike) { // Fail-fast but return locations like "(native)" if (urlLike.indexOf(':') === -1) { return [urlLike]; } let regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; let parts = regExp.exec(urlLike.replace(/[()]/g, '')); return [parts[1], parts[2] || undefined, parts[3] || undefined]; }, parseV8OrIE: function ErrorStackParser$$parseV8OrIE(error) { let filtered = error.stack.split('\n').filter(function(line) { return !!line.match(CHROME_IE_STACK_REGEXP); }, this); return filtered.map(function(line) { if (line.indexOf('(eval ') > -1) { // Throw away eval information until we implement stacktrace.js/stackframe#8 line = line .replace(/eval code/g, 'eval') .replace(/(\(eval at [^()]*)|(\),.*$)/g, ''); } let sanitizedLine = line .replace(/^\s+/, '') .replace(/\(eval code/g, '('); // capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in // case it has spaces in it, as the string is split on \s+ later on let location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/); // remove the parenthesized location from the line, if it was matched sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine; let tokens = sanitizedLine.split(/\s+/).slice(1); // if a location was matched, pass it to extractLocation() otherwise pop the last token let locationParts = this.extractLocation( location ? location[1] : tokens.pop() ); let functionName = tokens.join(' ') || undefined; let fileName = ['eval', ''].indexOf(locationParts[0]) > -1 ? undefined : locationParts[0]; return { functionName, fileName, lineNumber: locationParts[1], columnNumber: locationParts[2], source: line }; }, this); }, parseFFOrSafari: function ErrorStackParser$$parseFFOrSafari(error) { let filtered = error.stack.split('\n').filter(function(line) { return !line.match(SAFARI_NATIVE_CODE_REGEXP); }, this); return filtered.map(function(line) { // Throw away eval information until we implement stacktrace.js/stackframe#8 if (line.indexOf(' > eval') > -1) { line = line.replace( / line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1' ); } if (line.indexOf('@') === -1 && line.indexOf(':') === -1) { // Safari eval frames only have function names and nothing else return { functionName: line }; } else { let functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; let matches = line.match(functionNameRegex); let functionName = matches && matches[1] ? matches[1] : undefined; let locationParts = this.extractLocation( line.replace(functionNameRegex, '') ); return { functionName, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2], source: line }; } }, this); }, parseOpera: function ErrorStackParser$$parseOpera(e) { if ( !e.stacktrace || (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) ) { return this.parseOpera9(e); } else if (!e.stack) { return this.parseOpera10(e); } else { return this.parseOpera11(e); } }, parseOpera9: function ErrorStackParser$$parseOpera9(e) { let lineRE = /Line (\d+).*script (?:in )?(\S+)/i; let lines = e.message.split('\n'); let result = []; for (let i = 2, len = lines.length; i < len; i += 2) { let match = lineRE.exec(lines[i]); if (match) { result.push({ fileName: match[2], lineNumber: match[1], source: lines[i] }); } } return result; }, parseOpera10: function ErrorStackParser$$parseOpera10(e) { let lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; let lines = e.stacktrace.split('\n'); let result = []; for (let i = 0, len = lines.length; i < len; i += 2) { let match = lineRE.exec(lines[i]); if (match) { result.push({ functionName: match[3] || undefined, fileName: match[2], lineNumber: match[1], source: lines[i] }); } } return result; }, // Opera 10.65+ Error.stack very similar to FF/Safari parseOpera11: function ErrorStackParser$$parseOpera11(error) { let filtered = error.stack.split('\n').filter(function(line) { return ( !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/) ); }, this); return filtered.map(function(line) { let tokens = line.split('@'); let locationParts = this.extractLocation(tokens.pop()); let functionCall = tokens.shift() || ''; let functionName = functionCall .replace(//, '$2') .replace(/\([^)]*\)/g, '') || undefined; let argsRaw; if (functionCall.match(/\(([^)]*)\)/)) { argsRaw = functionCall.replace(/^[^(]+\(([^)]*)\)$/, '$1'); } let args = argsRaw === undefined || argsRaw === '[arguments not available]' ? undefined : argsRaw.split(','); return { functionName, args, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2], source: line }; }, this); } }; } // End borrow // wrapper exposing ErrorStackParser function stacktrace(p5, fn){ p5._getErrorStackParser = function getErrorStackParser() { return new ErrorStackParser(); }; } if (typeof p5 !== 'undefined') { stacktrace(p5); } /** A special constant with type `never` */ function $constructor(name, initializer, params) { function init(inst, def) { if (!inst._zod) { Object.defineProperty(inst, "_zod", { value: { def, constr: _, traits: new Set(), }, enumerable: false, }); } if (inst._zod.traits.has(name)) { return; } inst._zod.traits.add(name); initializer(inst, def); // support prototype modifications const proto = _.prototype; const keys = Object.keys(proto); for (let i = 0; i < keys.length; i++) { const k = keys[i]; if (!(k in inst)) { inst[k] = proto[k].bind(inst); } } } // doesn't work if Parent has a constructor with arguments const Parent = params?.Parent ?? Object; class Definition extends Parent { } Object.defineProperty(Definition, "name", { value: name }); function _(def) { var _a; const inst = params?.Parent ? new Definition() : this; init(inst, def); (_a = inst._zod).deferred ?? (_a.deferred = []); for (const fn of inst._zod.deferred) { fn(); } return inst; } Object.defineProperty(_, "init", { value: init }); Object.defineProperty(_, Symbol.hasInstance, { value: (inst) => { if (params?.Parent && inst instanceof params.Parent) return true; return inst?._zod?.traits?.has(name); }, }); Object.defineProperty(_, "name", { value: name }); return _; } class $ZodAsyncError extends Error { constructor() { super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`); } } class $ZodEncodeError extends Error { constructor(name) { super(`Encountered unidirectional transform during encode: ${name}`); this.name = "ZodEncodeError"; } } const globalConfig = {}; function config(newConfig) { return globalConfig; } // functions function getEnumValues(entries) { const numericValues = Object.values(entries).filter((v) => typeof v === "number"); const values = Object.entries(entries) .filter(([k, _]) => numericValues.indexOf(+k) === -1) .map(([_, v]) => v); return values; } function jsonStringifyReplacer(_, value) { if (typeof value === "bigint") return value.toString(); return value; } function cached(getter) { return { get value() { { const value = getter(); Object.defineProperty(this, "value", { value }); return value; } }, }; } function nullish(input) { return input === null || input === undefined; } function cleanRegex(source) { const start = source.startsWith("^") ? 1 : 0; const end = source.endsWith("$") ? source.length - 1 : source.length; return source.slice(start, end); } function floatSafeRemainder(val, step) { const valDecCount = (val.toString().split(".")[1] || "").length; const stepString = step.toString(); let stepDecCount = (stepString.split(".")[1] || "").length; if (stepDecCount === 0 && /\d?e-\d?/.test(stepString)) { const match = stepString.match(/\d?e-(\d?)/); if (match?.[1]) { stepDecCount = Number.parseInt(match[1]); } } const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); return (valInt % stepInt) / 10 ** decCount; } const EVALUATING = Symbol("evaluating"); function defineLazy(object, key, getter) { let value = undefined; Object.defineProperty(object, key, { get() { if (value === EVALUATING) { // Circular reference detected, return undefined to break the cycle return undefined; } if (value === undefined) { value = EVALUATING; value = getter(); } return value; }, set(v) { Object.defineProperty(object, key, { value: v, // configurable: true, }); // object[key] = v; }, configurable: true, }); } function assignProp(target, prop, value) { Object.defineProperty(target, prop, { value, writable: true, enumerable: true, configurable: true, }); } function mergeDefs(...defs) { const mergedDescriptors = {}; for (const def of defs) { const descriptors = Object.getOwnPropertyDescriptors(def); Object.assign(mergedDescriptors, descriptors); } return Object.defineProperties({}, mergedDescriptors); } function esc(str) { return JSON.stringify(str); } function slugify(input) { return input .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/[\s_-]+/g, "-") .replace(/^-+|-+$/g, ""); } const captureStackTrace = ("captureStackTrace" in Error ? Error.captureStackTrace : (..._args) => { }); function isObject(data) { return typeof data === "object" && data !== null && !Array.isArray(data); } const allowsEval = cached(() => { // @ts-ignore if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) { return false; } try { const F = Function; new F(""); return true; } catch (_) { return false; } }); function isPlainObject(o) { if (isObject(o) === false) return false; // modified constructor const ctor = o.constructor; if (ctor === undefined) return true; if (typeof ctor !== "function") return true; // modified prototype const prot = ctor.prototype; if (isObject(prot) === false) return false; // ctor doesn't have static `isPrototypeOf` if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) { return false; } return true; } function shallowClone(o) { if (isPlainObject(o)) return { ...o }; if (Array.isArray(o)) return [...o]; return o; } const propertyKeyTypes = new Set(["string", "number", "symbol"]); function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // zod-specific utils function clone(inst, def, params) { const cl = new inst._zod.constr(def ?? inst._zod.def); if (!def || params?.parent) cl._zod.parent = inst; return cl; } function normalizeParams(_params) { const params = _params; if (!params) return {}; if (typeof params === "string") return { error: () => params }; if (params?.message !== undefined) { if (params?.error !== undefined) throw new Error("Cannot specify both `message` and `error` params"); params.error = params.message; } delete params.message; if (typeof params.error === "string") return { ...params, error: () => params.error }; return params; } function optionalKeys(shape) { return Object.keys(shape).filter((k) => { return shape[k]._zod.optin === "optional" && shape[k]._zod.optout === "optional"; }); } const NUMBER_FORMAT_RANGES = { safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], int32: [-2147483648, 2147483647], uint32: [0, 4294967295], float32: [-34028234663852886e22, 3.4028234663852886e38], float64: [-Number.MAX_VALUE, Number.MAX_VALUE], }; function pick(schema, mask) { const currDef = schema._zod.def; const def = mergeDefs(schema._zod.def, { get shape() { const newShape = {}; for (const key in mask) { if (!(key in currDef.shape)) { throw new Error(`Unrecognized key: "${key}"`); } if (!mask[key]) continue; newShape[key] = currDef.shape[key]; } assignProp(this, "shape", newShape); // self-caching return newShape; }, checks: [], }); return clone(schema, def); } function omit(schema, mask) { const currDef = schema._zod.def; const def = mergeDefs(schema._zod.def, { get shape() { const newShape = { ...schema._zod.def.shape }; for (const key in mask) { if (!(key in currDef.shape)) { throw new Error(`Unrecognized key: "${key}"`); } if (!mask[key]) continue; delete newShape[key]; } assignProp(this, "shape", newShape); // self-caching return newShape; }, checks: [], }); return clone(schema, def); } function extend(schema, shape) { if (!isPlainObject(shape)) { throw new Error("Invalid input to extend: expected a plain object"); } const checks = schema._zod.def.checks; const hasChecks = checks && checks.length > 0; if (hasChecks) { throw new Error("Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead."); } const def = mergeDefs(schema._zod.def, { get shape() { const _shape = { ...schema._zod.def.shape, ...shape }; assignProp(this, "shape", _shape); // self-caching return _shape; }, checks: [], }); return clone(schema, def); } function safeExtend(schema, shape) { if (!isPlainObject(shape)) { throw new Error("Invalid input to safeExtend: expected a plain object"); } const def = { ...schema._zod.def, get shape() { const _shape = { ...schema._zod.def.shape, ...shape }; assignProp(this, "shape", _shape); // self-caching return _shape; }, checks: schema._zod.def.checks, }; return clone(schema, def); } function merge(a, b) { const def = mergeDefs(a._zod.def, { get shape() { const _shape = { ...a._zod.def.shape, ...b._zod.def.shape }; assignProp(this, "shape", _shape); // self-caching return _shape; }, get catchall() { return b._zod.def.catchall; }, checks: [], // delete existing checks }); return clone(a, def); } function partial(Class, schema, mask) { const def = mergeDefs(schema._zod.def, { get shape() { const oldShape = schema._zod.def.shape; const shape = { ...oldShape }; if (mask) { for (const key in mask) { if (!(key in oldShape)) { throw new Error(`Unrecognized key: "${key}"`); } if (!mask[key]) continue; // if (oldShape[key]!._zod.optin === "optional") continue; shape[key] = Class ? new Class({ type: "optional", innerType: oldShape[key], }) : oldShape[key]; } } else { for (const key in oldShape) { // if (oldShape[key]!._zod.optin === "optional") continue; shape[key] = Class ? new Class({ type: "optional", innerType: oldShape[key], }) : oldShape[key]; } } assignProp(this, "shape", shape); // self-caching return shape; }, checks: [], }); return clone(schema, def); } function required(Class, schema, mask) { const def = mergeDefs(schema._zod.def, { get shape() { const oldShape = schema._zod.def.shape; const shape = { ...oldShape }; if (mask) { for (const key in mask) { if (!(key in shape)) { throw new Error(`Unrecognized key: "${key}"`); } if (!mask[key]) continue; // overwrite with non-optional shape[key] = new Class({ type: "nonoptional", innerType: oldShape[key], }); } } else { for (const key in oldShape) { // overwrite with non-optional shape[key] = new Class({ type: "nonoptional", innerType: oldShape[key], }); } } assignProp(this, "shape", shape); // self-caching return shape; }, checks: [], }); return clone(schema, def); } // invalid_type | too_big | too_small | invalid_format | not_multiple_of | unrecognized_keys | invalid_union | invalid_key | invalid_element | invalid_value | custom function aborted(x, startIndex = 0) { if (x.aborted === true) return true; for (let i = startIndex; i < x.issues.length; i++) { if (x.issues[i]?.continue !== true) { return true; } } return false; } function prefixIssues(path, issues) { return issues.map((iss) => { var _a; (_a = iss).path ?? (_a.path = []); iss.path.unshift(path); return iss; }); } function unwrapMessage(message) { return typeof message === "string" ? message : message?.message; } function finalizeIssue(iss, ctx, config) { const full = { ...iss, path: iss.path ?? [] }; // for backwards compatibility if (!iss.message) { const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? "Invalid input"; full.message = message; } // delete (full as any).def; delete full.inst; delete full.continue; if (!ctx?.reportInput) { delete full.input; } return full; } function getLengthableOrigin(input) { if (Array.isArray(input)) return "array"; if (typeof input === "string") return "string"; return "unknown"; } function issue(...args) { const [iss, input, inst] = args; if (typeof iss === "string") { return { message: iss, code: "custom", input, inst, }; } return { ...iss }; } const initializer$1 = (inst, def) => { inst.name = "$ZodError"; Object.defineProperty(inst, "_zod", { value: inst._zod, enumerable: false, }); Object.defineProperty(inst, "issues", { value: def, enumerable: false, }); inst.message = JSON.stringify(def, jsonStringifyReplacer, 2); Object.defineProperty(inst, "toString", { value: () => inst.message, enumerable: false, }); }; const $ZodError = $constructor("$ZodError", initializer$1); const $ZodRealError = $constructor("$ZodError", initializer$1, { Parent: Error }); function flattenError(error, mapper = (issue) => issue.message) { const fieldErrors = {}; const formErrors = []; for (const sub of error.issues) { if (sub.path.length > 0) { fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; fieldErrors[sub.path[0]].push(mapper(sub)); } else { formErrors.push(mapper(sub)); } } return { formErrors, fieldErrors }; } function formatError(error, mapper = (issue) => issue.message) { const fieldErrors = { _errors: [] }; const processError = (error) => { for (const issue of error.issues) { if (issue.code === "invalid_union" && issue.errors.length) { issue.errors.map((issues) => processError({ issues })); } else if (issue.code === "invalid_key") { processError({ issues: issue.issues }); } else if (issue.code === "invalid_element") { processError({ issues: issue.issues }); } else if (issue.path.length === 0) { fieldErrors._errors.push(mapper(issue)); } else { let curr = fieldErrors; let i = 0; while (i < issue.path.length) { const el = issue.path[i]; const terminal = i === issue.path.length - 1; if (!terminal) { curr[el] = curr[el] || { _errors: [] }; } else { curr[el] = curr[el] || { _errors: [] }; curr[el]._errors.push(mapper(issue)); } curr = curr[el]; i++; } } } }; processError(error); return fieldErrors; } const _parse = (_Err) => (schema, value, _ctx, _params) => { const ctx = _ctx ? Object.assign(_ctx, { async: false }) : { async: false }; const result = schema._zod.run({ value, issues: [] }, ctx); if (result instanceof Promise) { throw new $ZodAsyncError(); } if (result.issues.length) { const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))); captureStackTrace(e, _params?.callee); throw e; } return result.value; }; const parse$2 = /* @__PURE__*/ _parse($ZodRealError); const _parseAsync = (_Err) => async (schema, value, _ctx, params) => { const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true }; let result = schema._zod.run({ value, issues: [] }, ctx); if (result instanceof Promise) result = await result; if (result.issues.length) { const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))); captureStackTrace(e, params?.callee); throw e; } return result.value; }; const parseAsync$1 = /* @__PURE__*/ _parseAsync($ZodRealError); const _safeParse = (_Err) => (schema, value, _ctx) => { const ctx = _ctx ? { ..._ctx, async: false } : { async: false }; const result = schema._zod.run({ value, issues: [] }, ctx); if (result instanceof Promise) { throw new $ZodAsyncError(); } return result.issues.length ? { success: false, error: new (_Err ?? $ZodError)(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))), } : { success: true, data: result.value }; }; const safeParse$1 = /* @__PURE__*/ _safeParse($ZodRealError); const _safeParseAsync = (_Err) => async (schema, value, _ctx) => { const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true }; let result = schema._zod.run({ value, issues: [] }, ctx); if (result instanceof Promise) result = await result; return result.issues.length ? { success: false, error: new _Err(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))), } : { success: true, data: result.value }; }; const safeParseAsync$1 = /* @__PURE__*/ _safeParseAsync($ZodRealError); const _encode = (_Err) => (schema, value, _ctx) => { const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" }; return _parse(_Err)(schema, value, ctx); }; const _decode = (_Err) => (schema, value, _ctx) => { return _parse(_Err)(schema, value, _ctx); }; const _encodeAsync = (_Err) => async (schema, value, _ctx) => { const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" }; return _parseAsync(_Err)(schema, value, ctx); }; const _decodeAsync = (_Err) => async (schema, value, _ctx) => { return _parseAsync(_Err)(schema, value, _ctx); }; const _safeEncode = (_Err) => (schema, value, _ctx) => { const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" }; return _safeParse(_Err)(schema, value, ctx); }; const _safeDecode = (_Err) => (schema, value, _ctx) => { return _safeParse(_Err)(schema, value, _ctx); }; const _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => { const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" }; return _safeParseAsync(_Err)(schema, value, ctx); }; const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => { return _safeParseAsync(_Err)(schema, value, _ctx); }; const cuid = /^[cC][^\s-]{8,}$/; const cuid2 = /^[0-9a-z]+$/; const ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/; const xid = /^[0-9a-vA-V]{20}$/; const ksuid = /^[A-Za-z0-9]{27}$/; const nanoid = /^[a-zA-Z0-9_-]{21}$/; /** ISO 8601-1 duration regex. Does not support the 8601-2 extensions like negative durations or fractional/negative components. */ const duration$1 = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/; /** A regex for any UUID-like identifier: 8-4-4-4-12 hex pattern */ const guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/; /** Returns a regex for validating an RFC 9562/4122 UUID. * * @param version Optionally specify a version 1-8. If no version is specified, all versions are supported. */ const uuid = (version) => { if (!version) return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/; return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`); }; /** Practical email validation */ const email = /^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/; // from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression const _emoji$1 = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; function emoji() { return new RegExp(_emoji$1, "u"); } const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; const ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/; const cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/; const cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; // https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript const base64$1 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/; const base64url = /^[A-Za-z0-9_-]*$/; // https://blog.stevenlevithan.com/archives/validate-phone-number#r4-3 (regex sans spaces) const e164 = /^\+(?:[0-9]){6,14}[0-9]$/; // const dateSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; const dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`; const date$1 = /*@__PURE__*/ new RegExp(`^${dateSource}$`); function timeSource(args) { const hhmm = `(?:[01]\\d|2[0-3]):[0-5]\\d`; const regex = typeof args.precision === "number" ? args.precision === -1 ? `${hhmm}` : args.precision === 0 ? `${hhmm}:[0-5]\\d` : `${hhmm}:[0-5]\\d\\.\\d{${args.precision}}` : `${hhmm}(?::[0-5]\\d(?:\\.\\d+)?)?`; return regex; } function time$1(args) { return new RegExp(`^${timeSource(args)}$`); } // Adapted from https://stackoverflow.com/a/3143231 function datetime$1(args) { const time = timeSource({ precision: args.precision }); const opts = ["Z"]; if (args.local) opts.push(""); // if (args.offset) opts.push(`([+-]\\d{2}:\\d{2})`); if (args.offset) opts.push(`([+-](?:[01]\\d|2[0-3]):[0-5]\\d)`); const timeRegex = `${time}(?:${opts.join("|")})`; return new RegExp(`^${dateSource}T(?:${timeRegex})$`); } const string$1 = (params) => { const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`; return new RegExp(`^${regex}$`); }; const integer = /^-?\d+$/; const number$1 = /^-?\d+(?:\.\d+)?/; const boolean$1 = /^(?:true|false)$/i; // regex for string with no uppercase letters const lowercase = /^[^A-Z]*$/; // regex for string with no lowercase letters const uppercase = /^[^a-z]*$/; // import { $ZodType } from "./schemas.js"; const $ZodCheck = /*@__PURE__*/ $constructor("$ZodCheck", (inst, def) => { var _a; inst._zod ?? (inst._zod = {}); inst._zod.def = def; (_a = inst._zod).onattach ?? (_a.onattach = []); }); const numericOriginMap = { number: "number", bigint: "bigint", object: "date", }; const $ZodCheckLessThan = /*@__PURE__*/ $constructor("$ZodCheckLessThan", (inst, def) => { $ZodCheck.init(inst, def); const origin = numericOriginMap[typeof def.value]; inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY; if (def.value < curr) { if (def.inclusive) bag.maximum = def.value; else bag.exclusiveMaximum = def.value; } }); inst._zod.check = (payload) => { if (def.inclusive ? payload.value <= def.value : payload.value < def.value) { return; } payload.issues.push({ origin, code: "too_big", maximum: def.value, input: payload.value, inclusive: def.inclusive, inst, continue: !def.abort, }); }; }); const $ZodCheckGreaterThan = /*@__PURE__*/ $constructor("$ZodCheckGreaterThan", (inst, def) => { $ZodCheck.init(inst, def); const origin = numericOriginMap[typeof def.value]; inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY; if (def.value > curr) { if (def.inclusive) bag.minimum = def.value; else bag.exclusiveMinimum = def.value; } }); inst._zod.check = (payload) => { if (def.inclusive ? payload.value >= def.value : payload.value > def.value) { return; } payload.issues.push({ origin, code: "too_small", minimum: def.value, input: payload.value, inclusive: def.inclusive, inst, continue: !def.abort, }); }; }); const $ZodCheckMultipleOf = /*@__PURE__*/ $constructor("$ZodCheckMultipleOf", (inst, def) => { $ZodCheck.init(inst, def); inst._zod.onattach.push((inst) => { var _a; (_a = inst._zod.bag).multipleOf ?? (_a.multipleOf = def.value); }); inst._zod.check = (payload) => { if (typeof payload.value !== typeof def.value) throw new Error("Cannot mix number and bigint in multiple_of check."); const isMultiple = typeof payload.value === "bigint" ? payload.value % def.value === BigInt(0) : floatSafeRemainder(payload.value, def.value) === 0; if (isMultiple) return; payload.issues.push({ origin: typeof payload.value, code: "not_multiple_of", divisor: def.value, input: payload.value, inst, continue: !def.abort, }); }; }); const $ZodCheckNumberFormat = /*@__PURE__*/ $constructor("$ZodCheckNumberFormat", (inst, def) => { $ZodCheck.init(inst, def); // no format checks def.format = def.format || "float64"; const isInt = def.format?.includes("int"); const origin = isInt ? "int" : "number"; const [minimum, maximum] = NUMBER_FORMAT_RANGES[def.format]; inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; bag.format = def.format; bag.minimum = minimum; bag.maximum = maximum; if (isInt) bag.pattern = integer; }); inst._zod.check = (payload) => { const input = payload.value; if (isInt) { if (!Number.isInteger(input)) { // invalid_format issue // payload.issues.push({ // expected: def.format, // format: def.format, // code: "invalid_format", // input, // inst, // }); // invalid_type issue payload.issues.push({ expected: origin, format: def.format, code: "invalid_type", continue: false, input, inst, }); return; // not_multiple_of issue // payload.issues.push({ // code: "not_multiple_of", // origin: "number", // input, // inst, // divisor: 1, // }); } if (!Number.isSafeInteger(input)) { if (input > 0) { // too_big payload.issues.push({ input, code: "too_big", maximum: Number.MAX_SAFE_INTEGER, note: "Integers must be within the safe integer range.", inst, origin, continue: !def.abort, }); } else { // too_small payload.issues.push({ input, code: "too_small", minimum: Number.MIN_SAFE_INTEGER, note: "Integers must be within the safe integer range.", inst, origin, continue: !def.abort, }); } return; } } if (input < minimum) { payload.issues.push({ origin: "number", input, code: "too_small", minimum, inclusive: true, inst, continue: !def.abort, }); } if (input > maximum) { payload.issues.push({ origin: "number", input, code: "too_big", maximum, inst, }); } }; }); const $ZodCheckMaxLength = /*@__PURE__*/ $constructor("$ZodCheckMaxLength", (inst, def) => { var _a; $ZodCheck.init(inst, def); (_a = inst._zod.def).when ?? (_a.when = (payload) => { const val = payload.value; return !nullish(val) && val.length !== undefined; }); inst._zod.onattach.push((inst) => { const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY); if (def.maximum < curr) inst._zod.bag.maximum = def.maximum; }); inst._zod.check = (payload) => { const input = payload.value; const length = input.length; if (length <= def.maximum) return; const origin = getLengthableOrigin(input); payload.issues.push({ origin, code: "too_big", maximum: def.maximum, inclusive: true, input, inst, continue: !def.abort, }); }; }); const $ZodCheckMinLength = /*@__PURE__*/ $constructor("$ZodCheckMinLength", (inst, def) => { var _a; $ZodCheck.init(inst, def); (_a = inst._zod.def).when ?? (_a.when = (payload) => { const val = payload.value; return !nullish(val) && val.length !== undefined; }); inst._zod.onattach.push((inst) => { const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY); if (def.minimum > curr) inst._zod.bag.minimum = def.minimum; }); inst._zod.check = (payload) => { const input = payload.value; const length = input.length; if (length >= def.minimum) return; const origin = getLengthableOrigin(input); payload.issues.push({ origin, code: "too_small", minimum: def.minimum, inclusive: true, input, inst, continue: !def.abort, }); }; }); const $ZodCheckLengthEquals = /*@__PURE__*/ $constructor("$ZodCheckLengthEquals", (inst, def) => { var _a; $ZodCheck.init(inst, def); (_a = inst._zod.def).when ?? (_a.when = (payload) => { const val = payload.value; return !nullish(val) && val.length !== undefined; }); inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; bag.minimum = def.length; bag.maximum = def.length; bag.length = def.length; }); inst._zod.check = (payload) => { const input = payload.value; const length = input.length; if (length === def.length) return; const origin = getLengthableOrigin(input); const tooBig = length > def.length; payload.issues.push({ origin, ...(tooBig ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length }), inclusive: true, exact: true, input: payload.value, inst, continue: !def.abort, }); }; }); const $ZodCheckStringFormat = /*@__PURE__*/ $constructor("$ZodCheckStringFormat", (inst, def) => { var _a, _b; $ZodCheck.init(inst, def); inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; bag.format = def.format; if (def.pattern) { bag.patterns ?? (bag.patterns = new Set()); bag.patterns.add(def.pattern); } }); if (def.pattern) (_a = inst._zod).check ?? (_a.check = (payload) => { def.pattern.lastIndex = 0; if (def.pattern.test(payload.value)) return; payload.issues.push({ origin: "string", code: "invalid_format", format: def.format, input: payload.value, ...(def.pattern ? { pattern: def.pattern.toString() } : {}), inst, continue: !def.abort, }); }); else (_b = inst._zod).check ?? (_b.check = () => { }); }); const $ZodCheckRegex = /*@__PURE__*/ $constructor("$ZodCheckRegex", (inst, def) => { $ZodCheckStringFormat.init(inst, def); inst._zod.check = (payload) => { def.pattern.lastIndex = 0; if (def.pattern.test(payload.value)) return; payload.issues.push({ origin: "string", code: "invalid_format", format: "regex", input: payload.value, pattern: def.pattern.toString(), inst, continue: !def.abort, }); }; }); const $ZodCheckLowerCase = /*@__PURE__*/ $constructor("$ZodCheckLowerCase", (inst, def) => { def.pattern ?? (def.pattern = lowercase); $ZodCheckStringFormat.init(inst, def); }); const $ZodCheckUpperCase = /*@__PURE__*/ $constructor("$ZodCheckUpperCase", (inst, def) => { def.pattern ?? (def.pattern = uppercase); $ZodCheckStringFormat.init(inst, def); }); const $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, def) => { $ZodCheck.init(inst, def); const escapedRegex = escapeRegex(def.includes); const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex); def.pattern = pattern; inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; bag.patterns ?? (bag.patterns = new Set()); bag.patterns.add(pattern); }); inst._zod.check = (payload) => { if (payload.value.includes(def.includes, def.position)) return; payload.issues.push({ origin: "string", code: "invalid_format", format: "includes", includes: def.includes, input: payload.value, inst, continue: !def.abort, }); }; }); const $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (inst, def) => { $ZodCheck.init(inst, def); const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`); def.pattern ?? (def.pattern = pattern); inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; bag.patterns ?? (bag.patterns = new Set()); bag.patterns.add(pattern); }); inst._zod.check = (payload) => { if (payload.value.startsWith(def.prefix)) return; payload.issues.push({ origin: "string", code: "invalid_format", format: "starts_with", prefix: def.prefix, input: payload.value, inst, continue: !def.abort, }); }; }); const $ZodCheckEndsWith = /*@__PURE__*/ $constructor("$ZodCheckEndsWith", (inst, def) => { $ZodCheck.init(inst, def); const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`); def.pattern ?? (def.pattern = pattern); inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; bag.patterns ?? (bag.patterns = new Set()); bag.patterns.add(pattern); }); inst._zod.check = (payload) => { if (payload.value.endsWith(def.suffix)) return; payload.issues.push({ origin: "string", code: "invalid_format", format: "ends_with", suffix: def.suffix, input: payload.value, inst, continue: !def.abort, }); }; }); const $ZodCheckOverwrite = /*@__PURE__*/ $constructor("$ZodCheckOverwrite", (inst, def) => { $ZodCheck.init(inst, def); inst._zod.check = (payload) => { payload.value = def.tx(payload.value); }; }); class Doc { constructor(args = []) { this.content = []; this.indent = 0; if (this) this.args = args; } indented(fn) { this.indent += 1; fn(this); this.indent -= 1; } write(arg) { if (typeof arg === "function") { arg(this, { execution: "sync" }); arg(this, { execution: "async" }); return; } const content = arg; const lines = content.split("\n").filter((x) => x); const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length)); const dedented = lines.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x); for (const line of dedented) { this.content.push(line); } } compile() { const F = Function; const args = this?.args; const content = this?.content ?? [``]; const lines = [...content.map((x) => ` ${x}`)]; // console.log(lines.join("\n")); return new F(...args, lines.join("\n")); } } const version$2 = { major: 4, minor: 2, patch: 1, }; const $ZodType = /*@__PURE__*/ $constructor("$ZodType", (inst, def) => { var _a; inst ?? (inst = {}); inst._zod.def = def; // set _def property inst._zod.bag = inst._zod.bag || {}; // initialize _bag object inst._zod.version = version$2; const checks = [...(inst._zod.def.checks ?? [])]; // if inst is itself a checks.$ZodCheck, run it as a check if (inst._zod.traits.has("$ZodCheck")) { checks.unshift(inst); } for (const ch of checks) { for (const fn of ch._zod.onattach) { fn(inst); } } if (checks.length === 0) { // deferred initializer // inst._zod.parse is not yet defined (_a = inst._zod).deferred ?? (_a.deferred = []); inst._zod.deferred?.push(() => { inst._zod.run = inst._zod.parse; }); } else { const runChecks = (payload, checks, ctx) => { let isAborted = aborted(payload); let asyncResult; for (const ch of checks) { if (ch._zod.def.when) { const shouldRun = ch._zod.def.when(payload); if (!shouldRun) continue; } else if (isAborted) { continue; } const currLen = payload.issues.length; const _ = ch._zod.check(payload); if (_ instanceof Promise && ctx?.async === false) { throw new $ZodAsyncError(); } if (asyncResult || _ instanceof Promise) { asyncResult = (asyncResult ?? Promise.resolve()).then(async () => { await _; const nextLen = payload.issues.length; if (nextLen === currLen) return; if (!isAborted) isAborted = aborted(payload, currLen); }); } else { const nextLen = payload.issues.length; if (nextLen === currLen) continue; if (!isAborted) isAborted = aborted(payload, currLen); } } if (asyncResult) { return asyncResult.then(() => { return payload; }); } return payload; }; const handleCanaryResult = (canary, payload, ctx) => { // abort if the canary is aborted if (aborted(canary)) { canary.aborted = true; return canary; } // run checks first, then const checkResult = runChecks(payload, checks, ctx); if (checkResult instanceof Promise) { if (ctx.async === false) throw new $ZodAsyncError(); return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx)); } return inst._zod.parse(checkResult, ctx); }; inst._zod.run = (payload, ctx) => { if (ctx.skipChecks) { return inst._zod.parse(payload, ctx); } if (ctx.direction === "backward") { // run canary // initial pass (no checks) const canary = inst._zod.parse({ value: payload.value, issues: [] }, { ...ctx, skipChecks: true }); if (canary instanceof Promise) { return canary.then((canary) => { return handleCanaryResult(canary, payload, ctx); }); } return handleCanaryResult(canary, payload, ctx); } // forward const result = inst._zod.parse(payload, ctx); if (result instanceof Promise) { if (ctx.async === false) throw new $ZodAsyncError(); return result.then((result) => runChecks(result, checks, ctx)); } return runChecks(result, checks, ctx); }; } inst["~standard"] = { validate: (value) => { try { const r = safeParse$1(inst, value); return r.success ? { value: r.data } : { issues: r.error?.issues }; } catch (_) { return safeParseAsync$1(inst, value).then((r) => (r.success ? { value: r.data } : { issues: r.error?.issues })); } }, vendor: "zod", version: 1, }; }); const $ZodString = /*@__PURE__*/ $constructor("$ZodString", (inst, def) => { $ZodType.init(inst, def); inst._zod.pattern = [...(inst?._zod.bag?.patterns ?? [])].pop() ?? string$1(inst._zod.bag); inst._zod.parse = (payload, _) => { if (def.coerce) try { payload.value = String(payload.value); } catch (_) { } if (typeof payload.value === "string") return payload; payload.issues.push({ expected: "string", code: "invalid_type", input: payload.value, inst, }); return payload; }; }); const $ZodStringFormat = /*@__PURE__*/ $constructor("$ZodStringFormat", (inst, def) => { // check initialization must come first $ZodCheckStringFormat.init(inst, def); $ZodString.init(inst, def); }); const $ZodGUID = /*@__PURE__*/ $constructor("$ZodGUID", (inst, def) => { def.pattern ?? (def.pattern = guid); $ZodStringFormat.init(inst, def); }); const $ZodUUID = /*@__PURE__*/ $constructor("$ZodUUID", (inst, def) => { if (def.version) { const versionMap = { v1: 1, v2: 2, v3: 3, v4: 4, v5: 5, v6: 6, v7: 7, v8: 8, }; const v = versionMap[def.version]; if (v === undefined) throw new Error(`Invalid UUID version: "${def.version}"`); def.pattern ?? (def.pattern = uuid(v)); } else def.pattern ?? (def.pattern = uuid()); $ZodStringFormat.init(inst, def); }); const $ZodEmail = /*@__PURE__*/ $constructor("$ZodEmail", (inst, def) => { def.pattern ?? (def.pattern = email); $ZodStringFormat.init(inst, def); }); const $ZodURL = /*@__PURE__*/ $constructor("$ZodURL", (inst, def) => { $ZodStringFormat.init(inst, def); inst._zod.check = (payload) => { try { // Trim whitespace from input const trimmed = payload.value.trim(); // @ts-ignore const url = new URL(trimmed); if (def.hostname) { def.hostname.lastIndex = 0; if (!def.hostname.test(url.hostname)) { payload.issues.push({ code: "invalid_format", format: "url", note: "Invalid hostname", pattern: def.hostname.source, input: payload.value, inst, continue: !def.abort, }); } } if (def.protocol) { def.protocol.lastIndex = 0; if (!def.protocol.test(url.protocol.endsWith(":") ? url.protocol.slice(0, -1) : url.protocol)) { payload.issues.push({ code: "invalid_format", format: "url", note: "Invalid protocol", pattern: def.protocol.source, input: payload.value, inst, continue: !def.abort, }); } } // Set the output value based on normalize flag if (def.normalize) { // Use normalized URL payload.value = url.href; } else { // Preserve the original input (trimmed) payload.value = trimmed; } return; } catch (_) { payload.issues.push({ code: "invalid_format", format: "url", input: payload.value, inst, continue: !def.abort, }); } }; }); const $ZodEmoji = /*@__PURE__*/ $constructor("$ZodEmoji", (inst, def) => { def.pattern ?? (def.pattern = emoji()); $ZodStringFormat.init(inst, def); }); const $ZodNanoID = /*@__PURE__*/ $constructor("$ZodNanoID", (inst, def) => { def.pattern ?? (def.pattern = nanoid); $ZodStringFormat.init(inst, def); }); const $ZodCUID = /*@__PURE__*/ $constructor("$ZodCUID", (inst, def) => { def.pattern ?? (def.pattern = cuid); $ZodStringFormat.init(inst, def); }); const $ZodCUID2 = /*@__PURE__*/ $constructor("$ZodCUID2", (inst, def) => { def.pattern ?? (def.pattern = cuid2); $ZodStringFormat.init(inst, def); }); const $ZodULID = /*@__PURE__*/ $constructor("$ZodULID", (inst, def) => { def.pattern ?? (def.pattern = ulid); $ZodStringFormat.init(inst, def); }); const $ZodXID = /*@__PURE__*/ $constructor("$ZodXID", (inst, def) => { def.pattern ?? (def.pattern = xid); $ZodStringFormat.init(inst, def); }); const $ZodKSUID = /*@__PURE__*/ $constructor("$ZodKSUID", (inst, def) => { def.pattern ?? (def.pattern = ksuid); $ZodStringFormat.init(inst, def); }); const $ZodISODateTime = /*@__PURE__*/ $constructor("$ZodISODateTime", (inst, def) => { def.pattern ?? (def.pattern = datetime$1(def)); $ZodStringFormat.init(inst, def); }); const $ZodISODate = /*@__PURE__*/ $constructor("$ZodISODate", (inst, def) => { def.pattern ?? (def.pattern = date$1); $ZodStringFormat.init(inst, def); }); const $ZodISOTime = /*@__PURE__*/ $constructor("$ZodISOTime", (inst, def) => { def.pattern ?? (def.pattern = time$1(def)); $ZodStringFormat.init(inst, def); }); const $ZodISODuration = /*@__PURE__*/ $constructor("$ZodISODuration", (inst, def) => { def.pattern ?? (def.pattern = duration$1); $ZodStringFormat.init(inst, def); }); const $ZodIPv4 = /*@__PURE__*/ $constructor("$ZodIPv4", (inst, def) => { def.pattern ?? (def.pattern = ipv4); $ZodStringFormat.init(inst, def); inst._zod.bag.format = `ipv4`; }); const $ZodIPv6 = /*@__PURE__*/ $constructor("$ZodIPv6", (inst, def) => { def.pattern ?? (def.pattern = ipv6); $ZodStringFormat.init(inst, def); inst._zod.bag.format = `ipv6`; inst._zod.check = (payload) => { try { // @ts-ignore new URL(`http://[${payload.value}]`); // return; } catch { payload.issues.push({ code: "invalid_format", format: "ipv6", input: payload.value, inst, continue: !def.abort, }); } }; }); const $ZodCIDRv4 = /*@__PURE__*/ $constructor("$ZodCIDRv4", (inst, def) => { def.pattern ?? (def.pattern = cidrv4); $ZodStringFormat.init(inst, def); }); const $ZodCIDRv6 = /*@__PURE__*/ $constructor("$ZodCIDRv6", (inst, def) => { def.pattern ?? (def.pattern = cidrv6); // not used for validation $ZodStringFormat.init(inst, def); inst._zod.check = (payload) => { const parts = payload.value.split("/"); try { if (parts.length !== 2) throw new Error(); const [address, prefix] = parts; if (!prefix) throw new Error(); const prefixNum = Number(prefix); if (`${prefixNum}` !== prefix) throw new Error(); if (prefixNum < 0 || prefixNum > 128) throw new Error(); // @ts-ignore new URL(`http://[${address}]`); } catch { payload.issues.push({ code: "invalid_format", format: "cidrv6", input: payload.value, inst, continue: !def.abort, }); } }; }); ////////////////////////////// ZodBase64 ////////////////////////////// function isValidBase64(data) { if (data === "") return true; if (data.length % 4 !== 0) return false; try { // @ts-ignore atob(data); return true; } catch { return false; } } const $ZodBase64 = /*@__PURE__*/ $constructor("$ZodBase64", (inst, def) => { def.pattern ?? (def.pattern = base64$1); $ZodStringFormat.init(inst, def); inst._zod.bag.contentEncoding = "base64"; inst._zod.check = (payload) => { if (isValidBase64(payload.value)) return; payload.issues.push({ code: "invalid_format", format: "base64", input: payload.value, inst, continue: !def.abort, }); }; }); ////////////////////////////// ZodBase64 ////////////////////////////// function isValidBase64URL(data) { if (!base64url.test(data)) return false; const base64 = data.replace(/[-_]/g, (c) => (c === "-" ? "+" : "/")); const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); return isValidBase64(padded); } const $ZodBase64URL = /*@__PURE__*/ $constructor("$ZodBase64URL", (inst, def) => { def.pattern ?? (def.pattern = base64url); $ZodStringFormat.init(inst, def); inst._zod.bag.contentEncoding = "base64url"; inst._zod.check = (payload) => { if (isValidBase64URL(payload.value)) return; payload.issues.push({ code: "invalid_format", format: "base64url", input: payload.value, inst, continue: !def.abort, }); }; }); const $ZodE164 = /*@__PURE__*/ $constructor("$ZodE164", (inst, def) => { def.pattern ?? (def.pattern = e164); $ZodStringFormat.init(inst, def); }); ////////////////////////////// ZodJWT ////////////////////////////// function isValidJWT(token, algorithm = null) { try { const tokensParts = token.split("."); if (tokensParts.length !== 3) return false; const [header] = tokensParts; if (!header) return false; // @ts-ignore const parsedHeader = JSON.parse(atob(header)); if ("typ" in parsedHeader && parsedHeader?.typ !== "JWT") return false; if (!parsedHeader.alg) return false; if (algorithm && (!("alg" in parsedHeader) || parsedHeader.alg !== algorithm)) return false; return true; } catch { return false; } } const $ZodJWT = /*@__PURE__*/ $constructor("$ZodJWT", (inst, def) => { $ZodStringFormat.init(inst, def); inst._zod.check = (payload) => { if (isValidJWT(payload.value, def.alg)) return; payload.issues.push({ code: "invalid_format", format: "jwt", input: payload.value, inst, continue: !def.abort, }); }; }); const $ZodNumber = /*@__PURE__*/ $constructor("$ZodNumber", (inst, def) => { $ZodType.init(inst, def); inst._zod.pattern = inst._zod.bag.pattern ?? number$1; inst._zod.parse = (payload, _ctx) => { if (def.coerce) try { payload.value = Number(payload.value); } catch (_) { } const input = payload.value; if (typeof input === "number" && !Number.isNaN(input) && Number.isFinite(input)) { return payload; } const received = typeof input === "number" ? Number.isNaN(input) ? "NaN" : !Number.isFinite(input) ? "Infinity" : undefined : undefined; payload.issues.push({ expected: "number", code: "invalid_type", input, inst, ...(received ? { received } : {}), }); return payload; }; }); const $ZodNumberFormat = /*@__PURE__*/ $constructor("$ZodNumberFormat", (inst, def) => { $ZodCheckNumberFormat.init(inst, def); $ZodNumber.init(inst, def); // no format checks }); const $ZodBoolean = /*@__PURE__*/ $constructor("$ZodBoolean", (inst, def) => { $ZodType.init(inst, def); inst._zod.pattern = boolean$1; inst._zod.parse = (payload, _ctx) => { if (def.coerce) try { payload.value = Boolean(payload.value); } catch (_) { } const input = payload.value; if (typeof input === "boolean") return payload; payload.issues.push({ expected: "boolean", code: "invalid_type", input, inst, }); return payload; }; }); const $ZodAny = /*@__PURE__*/ $constructor("$ZodAny", (inst, def) => { $ZodType.init(inst, def); inst._zod.parse = (payload) => payload; }); const $ZodUnknown = /*@__PURE__*/ $constructor("$ZodUnknown", (inst, def) => { $ZodType.init(inst, def); inst._zod.parse = (payload) => payload; }); const $ZodNever = /*@__PURE__*/ $constructor("$ZodNever", (inst, def) => { $ZodType.init(inst, def); inst._zod.parse = (payload, _ctx) => { payload.issues.push({ expected: "never", code: "invalid_type", input: payload.value, inst, }); return payload; }; }); function handleArrayResult(result, final, index) { if (result.issues.length) { final.issues.push(...prefixIssues(index, result.issues)); } final.value[index] = result.value; } const $ZodArray = /*@__PURE__*/ $constructor("$ZodArray", (inst, def) => { $ZodType.init(inst, def); inst._zod.parse = (payload, ctx) => { const input = payload.value; if (!Array.isArray(input)) { payload.issues.push({ expected: "array", code: "invalid_type", input, inst, }); return payload; } payload.value = Array(input.length); const proms = []; for (let i = 0; i < input.length; i++) { const item = input[i]; const result = def.element._zod.run({ value: item, issues: [], }, ctx); if (result instanceof Promise) { proms.push(result.then((result) => handleArrayResult(result, payload, i))); } else { handleArrayResult(result, payload, i); } } if (proms.length) { return Promise.all(proms).then(() => payload); } return payload; //handleArrayResultsAsync(parseResults, final); }; }); function handlePropertyResult(result, final, key, input) { if (result.issues.length) { final.issues.push(...prefixIssues(key, result.issues)); } if (result.value === undefined) { if (key in input) { final.value[key] = undefined; } } else { final.value[key] = result.value; } } function normalizeDef(def) { const keys = Object.keys(def.shape); for (const k of keys) { if (!def.shape?.[k]?._zod?.traits?.has("$ZodType")) { throw new Error(`Invalid element at key "${k}": expected a Zod schema`); } } const okeys = optionalKeys(def.shape); return { ...def, keys, keySet: new Set(keys), numKeys: keys.length, optionalKeys: new Set(okeys), }; } function handleCatchall(proms, input, payload, ctx, def, inst) { const unrecognized = []; // iterate over input keys const keySet = def.keySet; const _catchall = def.catchall._zod; const t = _catchall.def.type; for (const key in input) { if (keySet.has(key)) continue; if (t === "never") { unrecognized.push(key); continue; } const r = _catchall.run({ value: input[key], issues: [] }, ctx); if (r instanceof Promise) { proms.push(r.then((r) => handlePropertyResult(r, payload, key, input))); } else { handlePropertyResult(r, payload, key, input); } } if (unrecognized.length) { payload.issues.push({ code: "unrecognized_keys", keys: unrecognized, input, inst, }); } if (!proms.length) return payload; return Promise.all(proms).then(() => { return payload; }); } const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => { // requires cast because technically $ZodObject doesn't extend $ZodType.init(inst, def); // const sh = def.shape; const desc = Object.getOwnPropertyDescriptor(def, "shape"); if (!desc?.get) { const sh = def.shape; Object.defineProperty(def, "shape", { get: () => { const newSh = { ...sh }; Object.defineProperty(def, "shape", { value: newSh, }); return newSh; }, }); } const _normalized = cached(() => normalizeDef(def)); defineLazy(inst._zod, "propValues", () => { const shape = def.shape; const propValues = {}; for (const key in shape) { const field = shape[key]._zod; if (field.values) { propValues[key] ?? (propValues[key] = new Set()); for (const v of field.values) propValues[key].add(v); } } return propValues; }); const isObject$1 = isObject; const catchall = def.catchall; let value; inst._zod.parse = (payload, ctx) => { value ?? (value = _normalized.value); const input = payload.value; if (!isObject$1(input)) { payload.issues.push({ expected: "object", code: "invalid_type", input, inst, }); return payload; } payload.value = {}; const proms = []; const shape = value.shape; for (const key of value.keys) { const el = shape[key]; const r = el._zod.run({ value: input[key], issues: [] }, ctx); if (r instanceof Promise) { proms.push(r.then((r) => handlePropertyResult(r, payload, key, input))); } else { handlePropertyResult(r, payload, key, input); } } if (!catchall) { return proms.length ? Promise.all(proms).then(() => payload) : payload; } return handleCatchall(proms, input, payload, ctx, _normalized.value, inst); }; }); const $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => { // requires cast because technically $ZodObject doesn't extend $ZodObject.init(inst, def); const superParse = inst._zod.parse; const _normalized = cached(() => normalizeDef(def)); const generateFastpass = (shape) => { const doc = new Doc(["shape", "payload", "ctx"]); const normalized = _normalized.value; const parseStr = (key) => { const k = esc(key); return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`; }; doc.write(`const input = payload.value;`); const ids = Object.create(null); let counter = 0; for (const key of normalized.keys) { ids[key] = `key_${counter++}`; } // A: preserve key order { doc.write(`const newResult = {};`); for (const key of normalized.keys) { const id = ids[key]; const k = esc(key); doc.write(`const ${id} = ${parseStr(key)};`); doc.write(` if (${id}.issues.length) { payload.issues = payload.issues.concat(${id}.issues.map(iss => ({ ...iss, path: iss.path ? [${k}, ...iss.path] : [${k}] }))); } if (${id}.value === undefined) { if (${k} in input) { newResult[${k}] = undefined; } } else { newResult[${k}] = ${id}.value; } `); } doc.write(`payload.value = newResult;`); doc.write(`return payload;`); const fn = doc.compile(); return (payload, ctx) => fn(shape, payload, ctx); }; let fastpass; const isObject$1 = isObject; const jit = !globalConfig.jitless; const allowsEval$1 = allowsEval; const fastEnabled = jit && allowsEval$1.value; // && !def.catchall; const catchall = def.catchall; let value; inst._zod.parse = (payload, ctx) => { value ?? (value = _normalized.value); const input = payload.value; if (!isObject$1(input)) { payload.issues.push({ expected: "object", code: "invalid_type", input, inst, }); return payload; } if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) { // always synchronous if (!fastpass) fastpass = generateFastpass(def.shape); payload = fastpass(payload, ctx); if (!catchall) return payload; return handleCatchall([], input, payload, ctx, value, inst); } return superParse(payload, ctx); }; }); function handleUnionResults(results, final, inst, ctx) { for (const result of results) { if (result.issues.length === 0) { final.value = result.value; return final; } } const nonaborted = results.filter((r) => !aborted(r)); if (nonaborted.length === 1) { final.value = nonaborted[0].value; return nonaborted[0]; } final.issues.push({ code: "invalid_union", input: final.value, inst, errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config()))), }); return final; } const $ZodUnion = /*@__PURE__*/ $constructor("$ZodUnion", (inst, def) => { $ZodType.init(inst, def); defineLazy(inst._zod, "optin", () => def.options.some((o) => o._zod.optin === "optional") ? "optional" : undefined); defineLazy(inst._zod, "optout", () => def.options.some((o) => o._zod.optout === "optional") ? "optional" : undefined); defineLazy(inst._zod, "values", () => { if (def.options.every((o) => o._zod.values)) { return new Set(def.options.flatMap((option) => Array.from(option._zod.values))); } return undefined; }); defineLazy(inst._zod, "pattern", () => { if (def.options.every((o) => o._zod.pattern)) { const patterns = def.options.map((o) => o._zod.pattern); return new RegExp(`^(${patterns.map((p) => cleanRegex(p.source)).join("|")})$`); } return undefined; }); const single = def.options.length === 1; const first = def.options[0]._zod.run; inst._zod.parse = (payload, ctx) => { if (single) { return first(payload, ctx); } let async = false; const results = []; for (const option of def.options) { const result = option._zod.run({ value: payload.value, issues: [], }, ctx); if (result instanceof Promise) { results.push(result); async = true; } else { if (result.issues.length === 0) return result; results.push(result); } } if (!async) return handleUnionResults(results, payload, inst, ctx); return Promise.all(results).then((results) => { return handleUnionResults(results, payload, inst, ctx); }); }; }); const $ZodIntersection = /*@__PURE__*/ $constructor("$ZodIntersection", (inst, def) => { $ZodType.init(inst, def); inst._zod.parse = (payload, ctx) => { const input = payload.value; const left = def.left._zod.run({ value: input, issues: [] }, ctx); const right = def.right._zod.run({ value: input, issues: [] }, ctx); const async = left instanceof Promise || right instanceof Promise; if (async) { return Promise.all([left, right]).then(([left, right]) => { return handleIntersectionResults(payload, left, right); }); } return handleIntersectionResults(payload, left, right); }; }); function mergeValues(a, b) { // const aType = parse.t(a); // const bType = parse.t(b); if (a === b) { return { valid: true, data: a }; } if (a instanceof Date && b instanceof Date && +a === +b) { return { valid: true, data: a }; } if (isPlainObject(a) && isPlainObject(b)) { const bKeys = Object.keys(b); const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1); const newObj = { ...a, ...b }; for (const key of sharedKeys) { const sharedValue = mergeValues(a[key], b[key]); if (!sharedValue.valid) { return { valid: false, mergeErrorPath: [key, ...sharedValue.mergeErrorPath], }; } newObj[key] = sharedValue.data; } return { valid: true, data: newObj }; } if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) { return { valid: false, mergeErrorPath: [] }; } const newArray = []; for (let index = 0; index < a.length; index++) { const itemA = a[index]; const itemB = b[index]; const sharedValue = mergeValues(itemA, itemB); if (!sharedValue.valid) { return { valid: false, mergeErrorPath: [index, ...sharedValue.mergeErrorPath], }; } newArray.push(sharedValue.data); } return { valid: true, data: newArray }; } return { valid: false, mergeErrorPath: [] }; } function handleIntersectionResults(result, left, right) { if (left.issues.length) { result.issues.push(...left.issues); } if (right.issues.length) { result.issues.push(...right.issues); } if (aborted(result)) return result; const merged = mergeValues(left.value, right.value); if (!merged.valid) { throw new Error(`Unmergable intersection. Error path: ` + `${JSON.stringify(merged.mergeErrorPath)}`); } result.value = merged.data; return result; } const $ZodTuple = /*@__PURE__*/ $constructor("$ZodTuple", (inst, def) => { $ZodType.init(inst, def); const items = def.items; inst._zod.parse = (payload, ctx) => { const input = payload.value; if (!Array.isArray(input)) { payload.issues.push({ input, inst, expected: "tuple", code: "invalid_type", }); return payload; } payload.value = []; const proms = []; const reversedIndex = [...items].reverse().findIndex((item) => item._zod.optin !== "optional"); const optStart = reversedIndex === -1 ? 0 : items.length - reversedIndex; if (!def.rest) { const tooBig = input.length > items.length; const tooSmall = input.length < optStart - 1; if (tooBig || tooSmall) { payload.issues.push({ ...(tooBig ? { code: "too_big", maximum: items.length } : { code: "too_small", minimum: items.length }), input, inst, origin: "array", }); return payload; } } let i = -1; for (const item of items) { i++; if (i >= input.length) if (i >= optStart) continue; const result = item._zod.run({ value: input[i], issues: [], }, ctx); if (result instanceof Promise) { proms.push(result.then((result) => handleTupleResult(result, payload, i))); } else { handleTupleResult(result, payload, i); } } if (def.rest) { const rest = input.slice(items.length); for (const el of rest) { i++; const result = def.rest._zod.run({ value: el, issues: [], }, ctx); if (result instanceof Promise) { proms.push(result.then((result) => handleTupleResult(result, payload, i))); } else { handleTupleResult(result, payload, i); } } } if (proms.length) return Promise.all(proms).then(() => payload); return payload; }; }); function handleTupleResult(result, final, index) { if (result.issues.length) { final.issues.push(...prefixIssues(index, result.issues)); } final.value[index] = result.value; } const $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => { $ZodType.init(inst, def); const values = getEnumValues(def.entries); const valuesSet = new Set(values); inst._zod.values = valuesSet; inst._zod.pattern = new RegExp(`^(${values .filter((k) => propertyKeyTypes.has(typeof k)) .map((o) => (typeof o === "string" ? escapeRegex(o) : o.toString())) .join("|")})$`); inst._zod.parse = (payload, _ctx) => { const input = payload.value; if (valuesSet.has(input)) { return payload; } payload.issues.push({ code: "invalid_value", values, input, inst, }); return payload; }; }); const $ZodLiteral = /*@__PURE__*/ $constructor("$ZodLiteral", (inst, def) => { $ZodType.init(inst, def); if (def.values.length === 0) { throw new Error("Cannot create literal schema with no valid values"); } const values = new Set(def.values); inst._zod.values = values; inst._zod.pattern = new RegExp(`^(${def.values .map((o) => (typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o))) .join("|")})$`); inst._zod.parse = (payload, _ctx) => { const input = payload.value; if (values.has(input)) { return payload; } payload.issues.push({ code: "invalid_value", values: def.values, input, inst, }); return payload; }; }); const $ZodTransform = /*@__PURE__*/ $constructor("$ZodTransform", (inst, def) => { $ZodType.init(inst, def); inst._zod.parse = (payload, ctx) => { if (ctx.direction === "backward") { throw new $ZodEncodeError(inst.constructor.name); } const _out = def.transform(payload.value, payload); if (ctx.async) { const output = _out instanceof Promise ? _out : Promise.resolve(_out); return output.then((output) => { payload.value = output; return payload; }); } if (_out instanceof Promise) { throw new $ZodAsyncError(); } payload.value = _out; return payload; }; }); function handleOptionalResult(result, input) { if (result.issues.length && input === undefined) { return { issues: [], value: undefined }; } return result; } const $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => { $ZodType.init(inst, def); inst._zod.optin = "optional"; inst._zod.optout = "optional"; defineLazy(inst._zod, "values", () => { return def.innerType._zod.values ? new Set([...def.innerType._zod.values, undefined]) : undefined; }); defineLazy(inst._zod, "pattern", () => { const pattern = def.innerType._zod.pattern; return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : undefined; }); inst._zod.parse = (payload, ctx) => { if (def.innerType._zod.optin === "optional") { const result = def.innerType._zod.run(payload, ctx); if (result instanceof Promise) return result.then((r) => handleOptionalResult(r, payload.value)); return handleOptionalResult(result, payload.value); } if (payload.value === undefined) { return payload; } return def.innerType._zod.run(payload, ctx); }; }); const $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => { $ZodType.init(inst, def); defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); defineLazy(inst._zod, "pattern", () => { const pattern = def.innerType._zod.pattern; return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : undefined; }); defineLazy(inst._zod, "values", () => { return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : undefined; }); inst._zod.parse = (payload, ctx) => { // Forward direction (decode): allow null to pass through if (payload.value === null) return payload; return def.innerType._zod.run(payload, ctx); }; }); const $ZodDefault = /*@__PURE__*/ $constructor("$ZodDefault", (inst, def) => { $ZodType.init(inst, def); // inst._zod.qin = "true"; inst._zod.optin = "optional"; defineLazy(inst._zod, "values", () => def.innerType._zod.values); inst._zod.parse = (payload, ctx) => { if (ctx.direction === "backward") { return def.innerType._zod.run(payload, ctx); } // Forward direction (decode): apply defaults for undefined input if (payload.value === undefined) { payload.value = def.defaultValue; /** * $ZodDefault returns the default value immediately in forward direction. * It doesn't pass the default value into the validator ("prefault"). There's no reason to pass the default value through validation. The validity of the default is enforced by TypeScript statically. Otherwise, it's the responsibility of the user to ensure the default is valid. In the case of pipes with divergent in/out types, you can specify the default on the `in` schema of your ZodPipe to set a "prefault" for the pipe. */ return payload; } // Forward direction: continue with default handling const result = def.innerType._zod.run(payload, ctx); if (result instanceof Promise) { return result.then((result) => handleDefaultResult(result, def)); } return handleDefaultResult(result, def); }; }); function handleDefaultResult(payload, def) { if (payload.value === undefined) { payload.value = def.defaultValue; } return payload; } const $ZodPrefault = /*@__PURE__*/ $constructor("$ZodPrefault", (inst, def) => { $ZodType.init(inst, def); inst._zod.optin = "optional"; defineLazy(inst._zod, "values", () => def.innerType._zod.values); inst._zod.parse = (payload, ctx) => { if (ctx.direction === "backward") { return def.innerType._zod.run(payload, ctx); } // Forward direction (decode): apply prefault for undefined input if (payload.value === undefined) { payload.value = def.defaultValue; } return def.innerType._zod.run(payload, ctx); }; }); const $ZodNonOptional = /*@__PURE__*/ $constructor("$ZodNonOptional", (inst, def) => { $ZodType.init(inst, def); defineLazy(inst._zod, "values", () => { const v = def.innerType._zod.values; return v ? new Set([...v].filter((x) => x !== undefined)) : undefined; }); inst._zod.parse = (payload, ctx) => { const result = def.innerType._zod.run(payload, ctx); if (result instanceof Promise) { return result.then((result) => handleNonOptionalResult(result, inst)); } return handleNonOptionalResult(result, inst); }; }); function handleNonOptionalResult(payload, inst) { if (!payload.issues.length && payload.value === undefined) { payload.issues.push({ code: "invalid_type", expected: "nonoptional", input: payload.value, inst, }); } return payload; } const $ZodCatch = /*@__PURE__*/ $constructor("$ZodCatch", (inst, def) => { $ZodType.init(inst, def); defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); defineLazy(inst._zod, "values", () => def.innerType._zod.values); inst._zod.parse = (payload, ctx) => { if (ctx.direction === "backward") { return def.innerType._zod.run(payload, ctx); } // Forward direction (decode): apply catch logic const result = def.innerType._zod.run(payload, ctx); if (result instanceof Promise) { return result.then((result) => { payload.value = result.value; if (result.issues.length) { payload.value = def.catchValue({ ...payload, error: { issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())), }, input: payload.value, }); payload.issues = []; } return payload; }); } payload.value = result.value; if (result.issues.length) { payload.value = def.catchValue({ ...payload, error: { issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())), }, input: payload.value, }); payload.issues = []; } return payload; }; }); const $ZodPipe = /*@__PURE__*/ $constructor("$ZodPipe", (inst, def) => { $ZodType.init(inst, def); defineLazy(inst._zod, "values", () => def.in._zod.values); defineLazy(inst._zod, "optin", () => def.in._zod.optin); defineLazy(inst._zod, "optout", () => def.out._zod.optout); defineLazy(inst._zod, "propValues", () => def.in._zod.propValues); inst._zod.parse = (payload, ctx) => { if (ctx.direction === "backward") { const right = def.out._zod.run(payload, ctx); if (right instanceof Promise) { return right.then((right) => handlePipeResult(right, def.in, ctx)); } return handlePipeResult(right, def.in, ctx); } const left = def.in._zod.run(payload, ctx); if (left instanceof Promise) { return left.then((left) => handlePipeResult(left, def.out, ctx)); } return handlePipeResult(left, def.out, ctx); }; }); function handlePipeResult(left, next, ctx) { if (left.issues.length) { // prevent further checks left.aborted = true; return left; } return next._zod.run({ value: left.value, issues: left.issues }, ctx); } const $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => { $ZodType.init(inst, def); defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues); defineLazy(inst._zod, "values", () => def.innerType._zod.values); defineLazy(inst._zod, "optin", () => def.innerType?._zod?.optin); defineLazy(inst._zod, "optout", () => def.innerType?._zod?.optout); inst._zod.parse = (payload, ctx) => { if (ctx.direction === "backward") { return def.innerType._zod.run(payload, ctx); } const result = def.innerType._zod.run(payload, ctx); if (result instanceof Promise) { return result.then(handleReadonlyResult); } return handleReadonlyResult(result); }; }); function handleReadonlyResult(payload) { payload.value = Object.freeze(payload.value); return payload; } const $ZodFunction = /*@__PURE__*/ $constructor("$ZodFunction", (inst, def) => { $ZodType.init(inst, def); inst._def = def; inst._zod.def = def; inst.implement = (func) => { if (typeof func !== "function") { throw new Error("implement() must be called with a function"); } return function (...args) { const parsedArgs = inst._def.input ? parse$2(inst._def.input, args) : args; const result = Reflect.apply(func, this, parsedArgs); if (inst._def.output) { return parse$2(inst._def.output, result); } return result; }; }; inst.implementAsync = (func) => { if (typeof func !== "function") { throw new Error("implementAsync() must be called with a function"); } return async function (...args) { const parsedArgs = inst._def.input ? await parseAsync$1(inst._def.input, args) : args; const result = await Reflect.apply(func, this, parsedArgs); if (inst._def.output) { return await parseAsync$1(inst._def.output, result); } return result; }; }; inst._zod.parse = (payload, _ctx) => { if (typeof payload.value !== "function") { payload.issues.push({ code: "invalid_type", expected: "function", input: payload.value, inst, }); return payload; } // Check if output is a promise type to determine if we should use async implementation const hasPromiseOutput = inst._def.output && inst._def.output._zod.def.type === "promise"; if (hasPromiseOutput) { payload.value = inst.implementAsync(payload.value); } else { payload.value = inst.implement(payload.value); } return payload; }; inst.input = (...args) => { const F = inst.constructor; if (Array.isArray(args[0])) { return new F({ type: "function", input: new $ZodTuple({ type: "tuple", items: args[0], rest: args[1], }), output: inst._def.output, }); } return new F({ type: "function", input: args[0], output: inst._def.output, }); }; inst.output = (output) => { const F = inst.constructor; return new F({ type: "function", input: inst._def.input, output, }); }; return inst; }); const $ZodCustom = /*@__PURE__*/ $constructor("$ZodCustom", (inst, def) => { $ZodCheck.init(inst, def); $ZodType.init(inst, def); inst._zod.parse = (payload, _) => { return payload; }; inst._zod.check = (payload) => { const input = payload.value; const r = def.fn(input); if (r instanceof Promise) { return r.then((r) => handleRefineResult(r, payload, input, inst)); } handleRefineResult(r, payload, input, inst); return; }; }); function handleRefineResult(result, payload, input, inst) { if (!result) { const _iss = { code: "custom", input, inst, // incorporates params.error into issue reporting path: [...(inst._zod.def.path ?? [])], // incorporates params.error into issue reporting continue: !inst._zod.def.abort, // params: inst._zod.def.params, }; if (inst._zod.def.params) _iss.params = inst._zod.def.params; payload.issues.push(issue(_iss)); } } var _a; class $ZodRegistry { constructor() { this._map = new WeakMap(); this._idmap = new Map(); } add(schema, ..._meta) { const meta = _meta[0]; this._map.set(schema, meta); if (meta && typeof meta === "object" && "id" in meta) { if (this._idmap.has(meta.id)) { throw new Error(`ID ${meta.id} already exists in the registry`); } this._idmap.set(meta.id, schema); } return this; } clear() { this._map = new WeakMap(); this._idmap = new Map(); return this; } remove(schema) { const meta = this._map.get(schema); if (meta && typeof meta === "object" && "id" in meta) { this._idmap.delete(meta.id); } this._map.delete(schema); return this; } get(schema) { // return this._map.get(schema) as any; // inherit metadata const p = schema._zod.parent; if (p) { const pm = { ...(this.get(p) ?? {}) }; delete pm.id; // do not inherit id const f = { ...pm, ...this._map.get(schema) }; return Object.keys(f).length ? f : undefined; } return this._map.get(schema); } has(schema) { return this._map.has(schema); } } // registries function registry() { return new $ZodRegistry(); } (_a = globalThis).__zod_globalRegistry ?? (_a.__zod_globalRegistry = registry()); const globalRegistry = globalThis.__zod_globalRegistry; function _string(Class, params) { return new Class({ type: "string", ...normalizeParams(params), }); } function _email(Class, params) { return new Class({ type: "string", format: "email", check: "string_format", abort: false, ...normalizeParams(params), }); } function _guid(Class, params) { return new Class({ type: "string", format: "guid", check: "string_format", abort: false, ...normalizeParams(params), }); } function _uuid(Class, params) { return new Class({ type: "string", format: "uuid", check: "string_format", abort: false, ...normalizeParams(params), }); } function _uuidv4(Class, params) { return new Class({ type: "string", format: "uuid", check: "string_format", abort: false, version: "v4", ...normalizeParams(params), }); } function _uuidv6(Class, params) { return new Class({ type: "string", format: "uuid", check: "string_format", abort: false, version: "v6", ...normalizeParams(params), }); } function _uuidv7(Class, params) { return new Class({ type: "string", format: "uuid", check: "string_format", abort: false, version: "v7", ...normalizeParams(params), }); } function _url(Class, params) { return new Class({ type: "string", format: "url", check: "string_format", abort: false, ...normalizeParams(params), }); } function _emoji(Class, params) { return new Class({ type: "string", format: "emoji", check: "string_format", abort: false, ...normalizeParams(params), }); } function _nanoid(Class, params) { return new Class({ type: "string", format: "nanoid", check: "string_format", abort: false, ...normalizeParams(params), }); } function _cuid(Class, params) { return new Class({ type: "string", format: "cuid", check: "string_format", abort: false, ...normalizeParams(params), }); } function _cuid2(Class, params) { return new Class({ type: "string", format: "cuid2", check: "string_format", abort: false, ...normalizeParams(params), }); } function _ulid(Class, params) { return new Class({ type: "string", format: "ulid", check: "string_format", abort: false, ...normalizeParams(params), }); } function _xid(Class, params) { return new Class({ type: "string", format: "xid", check: "string_format", abort: false, ...normalizeParams(params), }); } function _ksuid(Class, params) { return new Class({ type: "string", format: "ksuid", check: "string_format", abort: false, ...normalizeParams(params), }); } function _ipv4(Class, params) { return new Class({ type: "string", format: "ipv4", check: "string_format", abort: false, ...normalizeParams(params), }); } function _ipv6(Class, params) { return new Class({ type: "string", format: "ipv6", check: "string_format", abort: false, ...normalizeParams(params), }); } function _cidrv4(Class, params) { return new Class({ type: "string", format: "cidrv4", check: "string_format", abort: false, ...normalizeParams(params), }); } function _cidrv6(Class, params) { return new Class({ type: "string", format: "cidrv6", check: "string_format", abort: false, ...normalizeParams(params), }); } function _base64(Class, params) { return new Class({ type: "string", format: "base64", check: "string_format", abort: false, ...normalizeParams(params), }); } function _base64url(Class, params) { return new Class({ type: "string", format: "base64url", check: "string_format", abort: false, ...normalizeParams(params), }); } function _e164(Class, params) { return new Class({ type: "string", format: "e164", check: "string_format", abort: false, ...normalizeParams(params), }); } function _jwt(Class, params) { return new Class({ type: "string", format: "jwt", check: "string_format", abort: false, ...normalizeParams(params), }); } function _isoDateTime(Class, params) { return new Class({ type: "string", format: "datetime", check: "string_format", offset: false, local: false, precision: null, ...normalizeParams(params), }); } function _isoDate(Class, params) { return new Class({ type: "string", format: "date", check: "string_format", ...normalizeParams(params), }); } function _isoTime(Class, params) { return new Class({ type: "string", format: "time", check: "string_format", precision: null, ...normalizeParams(params), }); } function _isoDuration(Class, params) { return new Class({ type: "string", format: "duration", check: "string_format", ...normalizeParams(params), }); } function _number(Class, params) { return new Class({ type: "number", checks: [], ...normalizeParams(params), }); } function _int(Class, params) { return new Class({ type: "number", check: "number_format", abort: false, format: "safeint", ...normalizeParams(params), }); } function _boolean(Class, params) { return new Class({ type: "boolean", ...normalizeParams(params), }); } function _any(Class) { return new Class({ type: "any", }); } function _unknown(Class) { return new Class({ type: "unknown", }); } function _never(Class, params) { return new Class({ type: "never", ...normalizeParams(params), }); } function _lt(value, params) { return new $ZodCheckLessThan({ check: "less_than", ...normalizeParams(params), value, inclusive: false, }); } function _lte(value, params) { return new $ZodCheckLessThan({ check: "less_than", ...normalizeParams(params), value, inclusive: true, }); } function _gt(value, params) { return new $ZodCheckGreaterThan({ check: "greater_than", ...normalizeParams(params), value, inclusive: false, }); } function _gte(value, params) { return new $ZodCheckGreaterThan({ check: "greater_than", ...normalizeParams(params), value, inclusive: true, }); } function _multipleOf(value, params) { return new $ZodCheckMultipleOf({ check: "multiple_of", ...normalizeParams(params), value, }); } function _maxLength(maximum, params) { const ch = new $ZodCheckMaxLength({ check: "max_length", ...normalizeParams(params), maximum, }); return ch; } function _minLength(minimum, params) { return new $ZodCheckMinLength({ check: "min_length", ...normalizeParams(params), minimum, }); } function _length(length, params) { return new $ZodCheckLengthEquals({ check: "length_equals", ...normalizeParams(params), length, }); } function _regex(pattern, params) { return new $ZodCheckRegex({ check: "string_format", format: "regex", ...normalizeParams(params), pattern, }); } function _lowercase(params) { return new $ZodCheckLowerCase({ check: "string_format", format: "lowercase", ...normalizeParams(params), }); } function _uppercase(params) { return new $ZodCheckUpperCase({ check: "string_format", format: "uppercase", ...normalizeParams(params), }); } function _includes(includes, params) { return new $ZodCheckIncludes({ check: "string_format", format: "includes", ...normalizeParams(params), includes, }); } function _startsWith(prefix, params) { return new $ZodCheckStartsWith({ check: "string_format", format: "starts_with", ...normalizeParams(params), prefix, }); } function _endsWith(suffix, params) { return new $ZodCheckEndsWith({ check: "string_format", format: "ends_with", ...normalizeParams(params), suffix, }); } function _overwrite(tx) { return new $ZodCheckOverwrite({ check: "overwrite", tx, }); } // normalize function _normalize(form) { return _overwrite((input) => input.normalize(form)); } // trim function _trim() { return _overwrite((input) => input.trim()); } // toLowerCase function _toLowerCase() { return _overwrite((input) => input.toLowerCase()); } // toUpperCase function _toUpperCase() { return _overwrite((input) => input.toUpperCase()); } // slugify function _slugify() { return _overwrite((input) => slugify(input)); } function _array(Class, element, params) { return new Class({ type: "array", element, // get element() { // return element; // }, ...normalizeParams(params), }); } function _custom(Class, fn, _params) { const norm = normalizeParams(_params); norm.abort ?? (norm.abort = true); // default to abort:false const schema = new Class({ type: "custom", check: "custom", fn: fn, ...norm, }); return schema; } // same as _custom but defaults to abort:false function _refine(Class, fn, _params) { const schema = new Class({ type: "custom", check: "custom", fn: fn, ...normalizeParams(_params), }); return schema; } function _superRefine(fn) { const ch = _check((payload) => { payload.addIssue = (issue$1) => { if (typeof issue$1 === "string") { payload.issues.push(issue(issue$1, payload.value, ch._zod.def)); } else { // for Zod 3 backwards compatibility const _issue = issue$1; if (_issue.fatal) _issue.continue = false; _issue.code ?? (_issue.code = "custom"); _issue.input ?? (_issue.input = payload.value); _issue.inst ?? (_issue.inst = ch); _issue.continue ?? (_issue.continue = !ch._zod.def.abort); // abort is always undefined, so this is always true... payload.issues.push(issue(_issue)); } }; return fn(payload.value, payload); }); return ch; } function _check(fn, params) { const ch = new $ZodCheck({ check: "custom", ...normalizeParams(params), }); ch._zod.check = fn; return ch; } // function initializeContext(inputs: JSONSchemaGeneratorParams): ToJSONSchemaContext { // return { // processor: inputs.processor, // metadataRegistry: inputs.metadata ?? globalRegistry, // target: inputs.target ?? "draft-2020-12", // unrepresentable: inputs.unrepresentable ?? "throw", // }; // } function initializeContext(params) { // Normalize target: convert old non-hyphenated versions to hyphenated versions let target = params?.target ?? "draft-2020-12"; if (target === "draft-4") target = "draft-04"; if (target === "draft-7") target = "draft-07"; return { processors: params.processors ?? {}, metadataRegistry: params?.metadata ?? globalRegistry, target, unrepresentable: params?.unrepresentable ?? "throw", override: params?.override ?? (() => { }), io: params?.io ?? "output", counter: 0, seen: new Map(), cycles: params?.cycles ?? "ref", reused: params?.reused ?? "inline", external: params?.external ?? undefined, }; } function process(schema, ctx, _params = { path: [], schemaPath: [] }) { var _a; const def = schema._zod.def; // check for schema in seens const seen = ctx.seen.get(schema); if (seen) { seen.count++; // check if cycle const isCycle = _params.schemaPath.includes(schema); if (isCycle) { seen.cycle = _params.path; } return seen.schema; } // initialize const result = { schema: {}, count: 1, cycle: undefined, path: _params.path }; ctx.seen.set(schema, result); // custom method overrides default behavior const overrideSchema = schema._zod.toJSONSchema?.(); if (overrideSchema) { result.schema = overrideSchema; } else { const params = { ..._params, schemaPath: [..._params.schemaPath, schema], path: _params.path, }; const parent = schema._zod.parent; if (parent) { // schema was cloned from another schema result.ref = parent; process(parent, ctx, params); ctx.seen.get(parent).isParent = true; } else if (schema._zod.processJSONSchema) { schema._zod.processJSONSchema(ctx, result.schema, params); } else { const _json = result.schema; const processor = ctx.processors[def.type]; if (!processor) { throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`); } processor(schema, ctx, _json, params); } } // metadata const meta = ctx.metadataRegistry.get(schema); if (meta) Object.assign(result.schema, meta); if (ctx.io === "input" && isTransforming(schema)) { // examples/defaults only apply to output type of pipe delete result.schema.examples; delete result.schema.default; } // set prefault as default if (ctx.io === "input" && result.schema._prefault) (_a = result.schema).default ?? (_a.default = result.schema._prefault); delete result.schema._prefault; // pulling fresh from ctx.seen in case it was overwritten const _result = ctx.seen.get(schema); return _result.schema; } function extractDefs(ctx, schema // params: EmitParams ) { // iterate over seen map; const root = ctx.seen.get(schema); if (!root) throw new Error("Unprocessed schema. This is a bug in Zod."); // returns a ref to the schema // defId will be empty if the ref points to an external schema (or #) const makeURI = (entry) => { // comparing the seen objects because sometimes // multiple schemas map to the same seen object. // e.g. lazy // external is configured const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions"; if (ctx.external) { const externalId = ctx.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${ctx.counter++}`; // check if schema is in the external registry const uriGenerator = ctx.external.uri ?? ((id) => id); if (externalId) { return { ref: uriGenerator(externalId) }; } // otherwise, add to __shared const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`; entry[1].defId = id; // set defId so it will be reused if needed return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` }; } if (entry[1] === root) { return { ref: "#" }; } // self-contained schema const uriPrefix = `#`; const defUriPrefix = `${uriPrefix}/${defsSegment}/`; const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`; return { defId, ref: defUriPrefix + defId }; }; // stored cached version in `def` property // remove all properties, set $ref const extractToDef = (entry) => { // if the schema is already a reference, do not extract it if (entry[1].schema.$ref) { return; } const seen = entry[1]; const { ref, defId } = makeURI(entry); seen.def = { ...seen.schema }; // defId won't be set if the schema is a reference to an external schema // or if the schema is the root schema if (defId) seen.defId = defId; // wipe away all properties except $ref const schema = seen.schema; for (const key in schema) { delete schema[key]; } schema.$ref = ref; }; // throw on cycles // break cycles if (ctx.cycles === "throw") { for (const entry of ctx.seen.entries()) { const seen = entry[1]; if (seen.cycle) { throw new Error("Cycle detected: " + `#/${seen.cycle?.join("/")}/` + '\n\nSet the `cycles` parameter to `"ref"` to resolve cyclical schemas with defs.'); } } } // extract schemas into $defs for (const entry of ctx.seen.entries()) { const seen = entry[1]; // convert root schema to # $ref if (schema === entry[0]) { extractToDef(entry); // this has special handling for the root schema continue; } // extract schemas that are in the external registry if (ctx.external) { const ext = ctx.external.registry.get(entry[0])?.id; if (schema !== entry[0] && ext) { extractToDef(entry); continue; } } // extract schemas with `id` meta const id = ctx.metadataRegistry.get(entry[0])?.id; if (id) { extractToDef(entry); continue; } // break cycles if (seen.cycle) { // any extractToDef(entry); continue; } // extract reused schemas if (seen.count > 1) { if (ctx.reused === "ref") { extractToDef(entry); // biome-ignore lint: continue; } } } } function finalize(ctx, schema) { // // iterate over seen map; const root = ctx.seen.get(schema); if (!root) throw new Error("Unprocessed schema. This is a bug in Zod."); // flatten _refs const flattenRef = (zodSchema) => { const seen = ctx.seen.get(zodSchema); const schema = seen.def ?? seen.schema; const _cached = { ...schema }; // already seen if (seen.ref === null) { return; } // flatten ref if defined const ref = seen.ref; seen.ref = null; // prevent recursion if (ref) { flattenRef(ref); // merge referenced schema into current const refSchema = ctx.seen.get(ref).schema; if (refSchema.$ref && (ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0")) { schema.allOf = schema.allOf ?? []; schema.allOf.push(refSchema); } else { Object.assign(schema, refSchema); Object.assign(schema, _cached); // prevent overwriting any fields in the original schema } } // execute overrides if (!seen.isParent) ctx.override({ zodSchema: zodSchema, jsonSchema: schema, path: seen.path ?? [], }); }; for (const entry of [...ctx.seen.entries()].reverse()) { flattenRef(entry[0]); } const result = {}; if (ctx.target === "draft-2020-12") { result.$schema = "https://json-schema.org/draft/2020-12/schema"; } else if (ctx.target === "draft-07") { result.$schema = "http://json-schema.org/draft-07/schema#"; } else if (ctx.target === "draft-04") { result.$schema = "http://json-schema.org/draft-04/schema#"; } else ; if (ctx.external?.uri) { const id = ctx.external.registry.get(schema)?.id; if (!id) throw new Error("Schema is missing an `id` property"); result.$id = ctx.external.uri(id); } Object.assign(result, root.def ?? root.schema); // build defs object const defs = ctx.external?.defs ?? {}; for (const entry of ctx.seen.entries()) { const seen = entry[1]; if (seen.def && seen.defId) { defs[seen.defId] = seen.def; } } // set definitions in result if (ctx.external) ; else { if (Object.keys(defs).length > 0) { if (ctx.target === "draft-2020-12") { result.$defs = defs; } else { result.definitions = defs; } } } try { // this "finalizes" this schema and ensures all cycles are removed // each call to finalize() is functionally independent // though the seen map is shared const finalized = JSON.parse(JSON.stringify(result)); Object.defineProperty(finalized, "~standard", { value: { ...schema["~standard"], jsonSchema: { input: createStandardJSONSchemaMethod(schema, "input"), output: createStandardJSONSchemaMethod(schema, "output"), }, }, enumerable: false, writable: false, }); return finalized; } catch (_err) { throw new Error("Error converting schema to JSON."); } } function isTransforming(_schema, _ctx) { const ctx = _ctx ?? { seen: new Set() }; if (ctx.seen.has(_schema)) return false; ctx.seen.add(_schema); const def = _schema._zod.def; if (def.type === "transform") return true; if (def.type === "array") return isTransforming(def.element, ctx); if (def.type === "set") return isTransforming(def.valueType, ctx); if (def.type === "lazy") return isTransforming(def.getter(), ctx); if (def.type === "promise" || def.type === "optional" || def.type === "nonoptional" || def.type === "nullable" || def.type === "readonly" || def.type === "default" || def.type === "prefault") { return isTransforming(def.innerType, ctx); } if (def.type === "intersection") { return isTransforming(def.left, ctx) || isTransforming(def.right, ctx); } if (def.type === "record" || def.type === "map") { return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx); } if (def.type === "pipe") { return isTransforming(def.in, ctx) || isTransforming(def.out, ctx); } if (def.type === "object") { for (const key in def.shape) { if (isTransforming(def.shape[key], ctx)) return true; } return false; } if (def.type === "union") { for (const option of def.options) { if (isTransforming(option, ctx)) return true; } return false; } if (def.type === "tuple") { for (const item of def.items) { if (isTransforming(item, ctx)) return true; } if (def.rest && isTransforming(def.rest, ctx)) return true; return false; } return false; } /** * Creates a toJSONSchema method for a schema instance. * This encapsulates the logic of initializing context, processing, extracting defs, and finalizing. */ const createToJSONSchemaMethod = (schema, processors = {}) => (params) => { const ctx = initializeContext({ ...params, processors }); process(schema, ctx); extractDefs(ctx, schema); return finalize(ctx, schema); }; const createStandardJSONSchemaMethod = (schema, io) => (params) => { const { libraryOptions, target } = params ?? {}; const ctx = initializeContext({ ...(libraryOptions ?? {}), target, io, processors: {} }); process(schema, ctx); extractDefs(ctx, schema); return finalize(ctx, schema); }; const formatMap = { guid: "uuid", url: "uri", datetime: "date-time", json_string: "json-string", regex: "", // do not set }; // ==================== SIMPLE TYPE PROCESSORS ==================== const stringProcessor = (schema, ctx, _json, _params) => { const json = _json; json.type = "string"; const { minimum, maximum, format, patterns, contentEncoding } = schema._zod .bag; if (typeof minimum === "number") json.minLength = minimum; if (typeof maximum === "number") json.maxLength = maximum; // custom pattern overrides format if (format) { json.format = formatMap[format] ?? format; if (json.format === "") delete json.format; // empty format is not valid } if (contentEncoding) json.contentEncoding = contentEncoding; if (patterns && patterns.size > 0) { const regexes = [...patterns]; if (regexes.length === 1) json.pattern = regexes[0].source; else if (regexes.length > 1) { json.allOf = [ ...regexes.map((regex) => ({ ...(ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0" ? { type: "string" } : {}), pattern: regex.source, })), ]; } } }; const numberProcessor = (schema, ctx, _json, _params) => { const json = _json; const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag; if (typeof format === "string" && format.includes("int")) json.type = "integer"; else json.type = "number"; if (typeof exclusiveMinimum === "number") { if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") { json.minimum = exclusiveMinimum; json.exclusiveMinimum = true; } else { json.exclusiveMinimum = exclusiveMinimum; } } if (typeof minimum === "number") { json.minimum = minimum; if (typeof exclusiveMinimum === "number" && ctx.target !== "draft-04") { if (exclusiveMinimum >= minimum) delete json.minimum; else delete json.exclusiveMinimum; } } if (typeof exclusiveMaximum === "number") { if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") { json.maximum = exclusiveMaximum; json.exclusiveMaximum = true; } else { json.exclusiveMaximum = exclusiveMaximum; } } if (typeof maximum === "number") { json.maximum = maximum; if (typeof exclusiveMaximum === "number" && ctx.target !== "draft-04") { if (exclusiveMaximum <= maximum) delete json.maximum; else delete json.exclusiveMaximum; } } if (typeof multipleOf === "number") json.multipleOf = multipleOf; }; const booleanProcessor = (_schema, _ctx, json, _params) => { json.type = "boolean"; }; const neverProcessor = (_schema, _ctx, json, _params) => { json.not = {}; }; const anyProcessor = (_schema, _ctx, _json, _params) => { // empty schema accepts anything }; const unknownProcessor = (_schema, _ctx, _json, _params) => { // empty schema accepts anything }; const enumProcessor = (schema, _ctx, json, _params) => { const def = schema._zod.def; const values = getEnumValues(def.entries); // Number enums can have both string and number values if (values.every((v) => typeof v === "number")) json.type = "number"; if (values.every((v) => typeof v === "string")) json.type = "string"; json.enum = values; }; const literalProcessor = (schema, ctx, json, _params) => { const def = schema._zod.def; const vals = []; for (const val of def.values) { if (val === undefined) { if (ctx.unrepresentable === "throw") { throw new Error("Literal `undefined` cannot be represented in JSON Schema"); } } else if (typeof val === "bigint") { if (ctx.unrepresentable === "throw") { throw new Error("BigInt literals cannot be represented in JSON Schema"); } else { vals.push(Number(val)); } } else { vals.push(val); } } if (vals.length === 0) ; else if (vals.length === 1) { const val = vals[0]; json.type = val === null ? "null" : typeof val; if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") { json.enum = [val]; } else { json.const = val; } } else { if (vals.every((v) => typeof v === "number")) json.type = "number"; if (vals.every((v) => typeof v === "string")) json.type = "string"; if (vals.every((v) => typeof v === "boolean")) json.type = "boolean"; if (vals.every((v) => v === null)) json.type = "null"; json.enum = vals; } }; const customProcessor = (_schema, ctx, _json, _params) => { if (ctx.unrepresentable === "throw") { throw new Error("Custom types cannot be represented in JSON Schema"); } }; const functionProcessor = (_schema, ctx, _json, _params) => { if (ctx.unrepresentable === "throw") { throw new Error("Function types cannot be represented in JSON Schema"); } }; const transformProcessor = (_schema, ctx, _json, _params) => { if (ctx.unrepresentable === "throw") { throw new Error("Transforms cannot be represented in JSON Schema"); } }; // ==================== COMPOSITE TYPE PROCESSORS ==================== const arrayProcessor = (schema, ctx, _json, params) => { const json = _json; const def = schema._zod.def; const { minimum, maximum } = schema._zod.bag; if (typeof minimum === "number") json.minItems = minimum; if (typeof maximum === "number") json.maxItems = maximum; json.type = "array"; json.items = process(def.element, ctx, { ...params, path: [...params.path, "items"] }); }; const objectProcessor = (schema, ctx, _json, params) => { const json = _json; const def = schema._zod.def; json.type = "object"; json.properties = {}; const shape = def.shape; for (const key in shape) { json.properties[key] = process(shape[key], ctx, { ...params, path: [...params.path, "properties", key], }); } // required keys const allKeys = new Set(Object.keys(shape)); const requiredKeys = new Set([...allKeys].filter((key) => { const v = def.shape[key]._zod; if (ctx.io === "input") { return v.optin === undefined; } else { return v.optout === undefined; } })); if (requiredKeys.size > 0) { json.required = Array.from(requiredKeys); } // catchall if (def.catchall?._zod.def.type === "never") { // strict json.additionalProperties = false; } else if (!def.catchall) { // regular if (ctx.io === "output") json.additionalProperties = false; } else if (def.catchall) { json.additionalProperties = process(def.catchall, ctx, { ...params, path: [...params.path, "additionalProperties"], }); } }; const unionProcessor = (schema, ctx, json, params) => { const def = schema._zod.def; // Exclusive unions (inclusive === false) use oneOf (exactly one match) instead of anyOf (one or more matches) // This includes both z.xor() and discriminated unions const isExclusive = def.inclusive === false; const options = def.options.map((x, i) => process(x, ctx, { ...params, path: [...params.path, isExclusive ? "oneOf" : "anyOf", i], })); if (isExclusive) { json.oneOf = options; } else { json.anyOf = options; } }; const intersectionProcessor = (schema, ctx, json, params) => { const def = schema._zod.def; const a = process(def.left, ctx, { ...params, path: [...params.path, "allOf", 0], }); const b = process(def.right, ctx, { ...params, path: [...params.path, "allOf", 1], }); const isSimpleIntersection = (val) => "allOf" in val && Object.keys(val).length === 1; const allOf = [ ...(isSimpleIntersection(a) ? a.allOf : [a]), ...(isSimpleIntersection(b) ? b.allOf : [b]), ]; json.allOf = allOf; }; const tupleProcessor = (schema, ctx, _json, params) => { const json = _json; const def = schema._zod.def; json.type = "array"; const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items"; const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems"; const prefixItems = def.items.map((x, i) => process(x, ctx, { ...params, path: [...params.path, prefixPath, i], })); const rest = def.rest ? process(def.rest, ctx, { ...params, path: [...params.path, restPath, ...(ctx.target === "openapi-3.0" ? [def.items.length] : [])], }) : null; if (ctx.target === "draft-2020-12") { json.prefixItems = prefixItems; if (rest) { json.items = rest; } } else if (ctx.target === "openapi-3.0") { json.items = { anyOf: prefixItems, }; if (rest) { json.items.anyOf.push(rest); } json.minItems = prefixItems.length; if (!rest) { json.maxItems = prefixItems.length; } } else { json.items = prefixItems; if (rest) { json.additionalItems = rest; } } // length const { minimum, maximum } = schema._zod.bag; if (typeof minimum === "number") json.minItems = minimum; if (typeof maximum === "number") json.maxItems = maximum; }; const nullableProcessor = (schema, ctx, json, params) => { const def = schema._zod.def; const inner = process(def.innerType, ctx, params); const seen = ctx.seen.get(schema); if (ctx.target === "openapi-3.0") { seen.ref = def.innerType; json.nullable = true; } else { json.anyOf = [inner, { type: "null" }]; } }; const nonoptionalProcessor = (schema, ctx, _json, params) => { const def = schema._zod.def; process(def.innerType, ctx, params); const seen = ctx.seen.get(schema); seen.ref = def.innerType; }; const defaultProcessor = (schema, ctx, json, params) => { const def = schema._zod.def; process(def.innerType, ctx, params); const seen = ctx.seen.get(schema); seen.ref = def.innerType; json.default = JSON.parse(JSON.stringify(def.defaultValue)); }; const prefaultProcessor = (schema, ctx, json, params) => { const def = schema._zod.def; process(def.innerType, ctx, params); const seen = ctx.seen.get(schema); seen.ref = def.innerType; if (ctx.io === "input") json._prefault = JSON.parse(JSON.stringify(def.defaultValue)); }; const catchProcessor = (schema, ctx, json, params) => { const def = schema._zod.def; process(def.innerType, ctx, params); const seen = ctx.seen.get(schema); seen.ref = def.innerType; let catchValue; try { catchValue = def.catchValue(undefined); } catch { throw new Error("Dynamic catch values are not supported in JSON Schema"); } json.default = catchValue; }; const pipeProcessor = (schema, ctx, _json, params) => { const def = schema._zod.def; const innerType = ctx.io === "input" ? (def.in._zod.def.type === "transform" ? def.out : def.in) : def.out; process(innerType, ctx, params); const seen = ctx.seen.get(schema); seen.ref = innerType; }; const readonlyProcessor = (schema, ctx, json, params) => { const def = schema._zod.def; process(def.innerType, ctx, params); const seen = ctx.seen.get(schema); seen.ref = def.innerType; json.readOnly = true; }; const optionalProcessor = (schema, ctx, _json, params) => { const def = schema._zod.def; process(def.innerType, ctx, params); const seen = ctx.seen.get(schema); seen.ref = def.innerType; }; const ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => { $ZodISODateTime.init(inst, def); ZodStringFormat.init(inst, def); }); function datetime(params) { return _isoDateTime(ZodISODateTime, params); } const ZodISODate = /*@__PURE__*/ $constructor("ZodISODate", (inst, def) => { $ZodISODate.init(inst, def); ZodStringFormat.init(inst, def); }); function date(params) { return _isoDate(ZodISODate, params); } const ZodISOTime = /*@__PURE__*/ $constructor("ZodISOTime", (inst, def) => { $ZodISOTime.init(inst, def); ZodStringFormat.init(inst, def); }); function time(params) { return _isoTime(ZodISOTime, params); } const ZodISODuration = /*@__PURE__*/ $constructor("ZodISODuration", (inst, def) => { $ZodISODuration.init(inst, def); ZodStringFormat.init(inst, def); }); function duration(params) { return _isoDuration(ZodISODuration, params); } const initializer = (inst, issues) => { $ZodError.init(inst, issues); inst.name = "ZodError"; Object.defineProperties(inst, { format: { value: (mapper) => formatError(inst, mapper), // enumerable: false, }, flatten: { value: (mapper) => flattenError(inst, mapper), // enumerable: false, }, addIssue: { value: (issue) => { inst.issues.push(issue); inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2); }, // enumerable: false, }, addIssues: { value: (issues) => { inst.issues.push(...issues); inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2); }, // enumerable: false, }, isEmpty: { get() { return inst.issues.length === 0; }, // enumerable: false, }, }); // Object.defineProperty(inst, "isEmpty", { // get() { // return inst.issues.length === 0; // }, // }); }; const ZodRealError = $constructor("ZodError", initializer, { Parent: Error, }); // /** @deprecated Use `z.core.$ZodErrorMapCtx` instead. */ // export type ErrorMapCtx = core.$ZodErrorMapCtx; const parse$1 = /* @__PURE__ */ _parse(ZodRealError); const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError); const safeParse = /* @__PURE__ */ _safeParse(ZodRealError); const safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError); // Codec functions const encode = /* @__PURE__ */ _encode(ZodRealError); const decode = /* @__PURE__ */ _decode(ZodRealError); const encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError); const decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError); const safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError); const safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError); const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError); const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError); const ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => { $ZodType.init(inst, def); Object.assign(inst["~standard"], { jsonSchema: { input: createStandardJSONSchemaMethod(inst, "input"), output: createStandardJSONSchemaMethod(inst, "output"), }, }); inst.toJSONSchema = createToJSONSchemaMethod(inst, {}); inst.def = def; inst.type = def.type; Object.defineProperty(inst, "_def", { value: def }); // base methods inst.check = (...checks) => { return inst.clone(mergeDefs(def, { checks: [ ...(def.checks ?? []), ...checks.map((ch) => typeof ch === "function" ? { _zod: { check: ch, def: { check: "custom" }, onattach: [] } } : ch), ], })); }; inst.clone = (def, params) => clone(inst, def, params); inst.brand = () => inst; inst.register = ((reg, meta) => { reg.add(inst, meta); return inst; }); // parsing inst.parse = (data, params) => parse$1(inst, data, params, { callee: inst.parse }); inst.safeParse = (data, params) => safeParse(inst, data, params); inst.parseAsync = async (data, params) => parseAsync(inst, data, params, { callee: inst.parseAsync }); inst.safeParseAsync = async (data, params) => safeParseAsync(inst, data, params); inst.spa = inst.safeParseAsync; // encoding/decoding inst.encode = (data, params) => encode(inst, data, params); inst.decode = (data, params) => decode(inst, data, params); inst.encodeAsync = async (data, params) => encodeAsync(inst, data, params); inst.decodeAsync = async (data, params) => decodeAsync(inst, data, params); inst.safeEncode = (data, params) => safeEncode(inst, data, params); inst.safeDecode = (data, params) => safeDecode(inst, data, params); inst.safeEncodeAsync = async (data, params) => safeEncodeAsync(inst, data, params); inst.safeDecodeAsync = async (data, params) => safeDecodeAsync(inst, data, params); // refinements inst.refine = (check, params) => inst.check(refine(check, params)); inst.superRefine = (refinement) => inst.check(superRefine(refinement)); inst.overwrite = (fn) => inst.check(_overwrite(fn)); // wrappers inst.optional = () => optional(inst); inst.nullable = () => nullable(inst); inst.nullish = () => optional(nullable(inst)); inst.nonoptional = (params) => nonoptional(inst, params); inst.array = () => array(inst); inst.or = (arg) => union([inst, arg]); inst.and = (arg) => intersection(inst, arg); inst.transform = (tx) => pipe(inst, transform(tx)); inst.default = (def) => _default(inst, def); inst.prefault = (def) => prefault(inst, def); // inst.coalesce = (def, params) => coalesce(inst, def, params); inst.catch = (params) => _catch(inst, params); inst.pipe = (target) => pipe(inst, target); inst.readonly = () => readonly(inst); // meta inst.describe = (description) => { const cl = inst.clone(); globalRegistry.add(cl, { description }); return cl; }; Object.defineProperty(inst, "description", { get() { return globalRegistry.get(inst)?.description; }, configurable: true, }); inst.meta = (...args) => { if (args.length === 0) { return globalRegistry.get(inst); } const cl = inst.clone(); globalRegistry.add(cl, args[0]); return cl; }; // helpers inst.isOptional = () => inst.safeParse(undefined).success; inst.isNullable = () => inst.safeParse(null).success; return inst; }); /** @internal */ const _ZodString = /*@__PURE__*/ $constructor("_ZodString", (inst, def) => { $ZodString.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => stringProcessor(inst, ctx, json); const bag = inst._zod.bag; inst.format = bag.format ?? null; inst.minLength = bag.minimum ?? null; inst.maxLength = bag.maximum ?? null; // validations inst.regex = (...args) => inst.check(_regex(...args)); inst.includes = (...args) => inst.check(_includes(...args)); inst.startsWith = (...args) => inst.check(_startsWith(...args)); inst.endsWith = (...args) => inst.check(_endsWith(...args)); inst.min = (...args) => inst.check(_minLength(...args)); inst.max = (...args) => inst.check(_maxLength(...args)); inst.length = (...args) => inst.check(_length(...args)); inst.nonempty = (...args) => inst.check(_minLength(1, ...args)); inst.lowercase = (params) => inst.check(_lowercase(params)); inst.uppercase = (params) => inst.check(_uppercase(params)); // transforms inst.trim = () => inst.check(_trim()); inst.normalize = (...args) => inst.check(_normalize(...args)); inst.toLowerCase = () => inst.check(_toLowerCase()); inst.toUpperCase = () => inst.check(_toUpperCase()); inst.slugify = () => inst.check(_slugify()); }); const ZodString = /*@__PURE__*/ $constructor("ZodString", (inst, def) => { $ZodString.init(inst, def); _ZodString.init(inst, def); inst.email = (params) => inst.check(_email(ZodEmail, params)); inst.url = (params) => inst.check(_url(ZodURL, params)); inst.jwt = (params) => inst.check(_jwt(ZodJWT, params)); inst.emoji = (params) => inst.check(_emoji(ZodEmoji, params)); inst.guid = (params) => inst.check(_guid(ZodGUID, params)); inst.uuid = (params) => inst.check(_uuid(ZodUUID, params)); inst.uuidv4 = (params) => inst.check(_uuidv4(ZodUUID, params)); inst.uuidv6 = (params) => inst.check(_uuidv6(ZodUUID, params)); inst.uuidv7 = (params) => inst.check(_uuidv7(ZodUUID, params)); inst.nanoid = (params) => inst.check(_nanoid(ZodNanoID, params)); inst.guid = (params) => inst.check(_guid(ZodGUID, params)); inst.cuid = (params) => inst.check(_cuid(ZodCUID, params)); inst.cuid2 = (params) => inst.check(_cuid2(ZodCUID2, params)); inst.ulid = (params) => inst.check(_ulid(ZodULID, params)); inst.base64 = (params) => inst.check(_base64(ZodBase64, params)); inst.base64url = (params) => inst.check(_base64url(ZodBase64URL, params)); inst.xid = (params) => inst.check(_xid(ZodXID, params)); inst.ksuid = (params) => inst.check(_ksuid(ZodKSUID, params)); inst.ipv4 = (params) => inst.check(_ipv4(ZodIPv4, params)); inst.ipv6 = (params) => inst.check(_ipv6(ZodIPv6, params)); inst.cidrv4 = (params) => inst.check(_cidrv4(ZodCIDRv4, params)); inst.cidrv6 = (params) => inst.check(_cidrv6(ZodCIDRv6, params)); inst.e164 = (params) => inst.check(_e164(ZodE164, params)); // iso inst.datetime = (params) => inst.check(datetime(params)); inst.date = (params) => inst.check(date(params)); inst.time = (params) => inst.check(time(params)); inst.duration = (params) => inst.check(duration(params)); }); function string(params) { return _string(ZodString, params); } const ZodStringFormat = /*@__PURE__*/ $constructor("ZodStringFormat", (inst, def) => { $ZodStringFormat.init(inst, def); _ZodString.init(inst, def); }); const ZodEmail = /*@__PURE__*/ $constructor("ZodEmail", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodEmail.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodGUID = /*@__PURE__*/ $constructor("ZodGUID", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodGUID.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodUUID = /*@__PURE__*/ $constructor("ZodUUID", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodUUID.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodURL = /*@__PURE__*/ $constructor("ZodURL", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodURL.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodEmoji = /*@__PURE__*/ $constructor("ZodEmoji", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodEmoji.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodNanoID = /*@__PURE__*/ $constructor("ZodNanoID", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodNanoID.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodCUID = /*@__PURE__*/ $constructor("ZodCUID", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodCUID.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodCUID2 = /*@__PURE__*/ $constructor("ZodCUID2", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodCUID2.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodULID = /*@__PURE__*/ $constructor("ZodULID", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodULID.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodXID = /*@__PURE__*/ $constructor("ZodXID", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodXID.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodKSUID = /*@__PURE__*/ $constructor("ZodKSUID", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodKSUID.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodIPv4 = /*@__PURE__*/ $constructor("ZodIPv4", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodIPv4.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodIPv6 = /*@__PURE__*/ $constructor("ZodIPv6", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodIPv6.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodCIDRv4 = /*@__PURE__*/ $constructor("ZodCIDRv4", (inst, def) => { $ZodCIDRv4.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodCIDRv6 = /*@__PURE__*/ $constructor("ZodCIDRv6", (inst, def) => { $ZodCIDRv6.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodBase64 = /*@__PURE__*/ $constructor("ZodBase64", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodBase64.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodBase64URL = /*@__PURE__*/ $constructor("ZodBase64URL", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodBase64URL.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodE164 = /*@__PURE__*/ $constructor("ZodE164", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodE164.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodJWT = /*@__PURE__*/ $constructor("ZodJWT", (inst, def) => { // ZodStringFormat.init(inst, def); $ZodJWT.init(inst, def); ZodStringFormat.init(inst, def); }); const ZodNumber = /*@__PURE__*/ $constructor("ZodNumber", (inst, def) => { $ZodNumber.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => numberProcessor(inst, ctx, json); inst.gt = (value, params) => inst.check(_gt(value, params)); inst.gte = (value, params) => inst.check(_gte(value, params)); inst.min = (value, params) => inst.check(_gte(value, params)); inst.lt = (value, params) => inst.check(_lt(value, params)); inst.lte = (value, params) => inst.check(_lte(value, params)); inst.max = (value, params) => inst.check(_lte(value, params)); inst.int = (params) => inst.check(int(params)); inst.safe = (params) => inst.check(int(params)); inst.positive = (params) => inst.check(_gt(0, params)); inst.nonnegative = (params) => inst.check(_gte(0, params)); inst.negative = (params) => inst.check(_lt(0, params)); inst.nonpositive = (params) => inst.check(_lte(0, params)); inst.multipleOf = (value, params) => inst.check(_multipleOf(value, params)); inst.step = (value, params) => inst.check(_multipleOf(value, params)); // inst.finite = (params) => inst.check(core.finite(params)); inst.finite = () => inst; const bag = inst._zod.bag; inst.minValue = Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null; inst.maxValue = Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null; inst.isInt = (bag.format ?? "").includes("int") || Number.isSafeInteger(bag.multipleOf ?? 0.5); inst.isFinite = true; inst.format = bag.format ?? null; }); function number(params) { return _number(ZodNumber, params); } const ZodNumberFormat = /*@__PURE__*/ $constructor("ZodNumberFormat", (inst, def) => { $ZodNumberFormat.init(inst, def); ZodNumber.init(inst, def); }); function int(params) { return _int(ZodNumberFormat, params); } const ZodBoolean = /*@__PURE__*/ $constructor("ZodBoolean", (inst, def) => { $ZodBoolean.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json); }); function boolean(params) { return _boolean(ZodBoolean, params); } const ZodAny = /*@__PURE__*/ $constructor("ZodAny", (inst, def) => { $ZodAny.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => anyProcessor(); }); function any() { return _any(ZodAny); } const ZodUnknown = /*@__PURE__*/ $constructor("ZodUnknown", (inst, def) => { $ZodUnknown.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => unknownProcessor(); }); function unknown() { return _unknown(ZodUnknown); } const ZodNever = /*@__PURE__*/ $constructor("ZodNever", (inst, def) => { $ZodNever.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => neverProcessor(inst, ctx, json); }); function never(params) { return _never(ZodNever, params); } const ZodArray = /*@__PURE__*/ $constructor("ZodArray", (inst, def) => { $ZodArray.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => arrayProcessor(inst, ctx, json, params); inst.element = def.element; inst.min = (minLength, params) => inst.check(_minLength(minLength, params)); inst.nonempty = (params) => inst.check(_minLength(1, params)); inst.max = (maxLength, params) => inst.check(_maxLength(maxLength, params)); inst.length = (len, params) => inst.check(_length(len, params)); inst.unwrap = () => inst.element; }); function array(element, params) { return _array(ZodArray, element, params); } const ZodObject = /*@__PURE__*/ $constructor("ZodObject", (inst, def) => { $ZodObjectJIT.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => objectProcessor(inst, ctx, json, params); defineLazy(inst, "shape", () => { return def.shape; }); inst.keyof = () => _enum(Object.keys(inst._zod.def.shape)); inst.catchall = (catchall) => inst.clone({ ...inst._zod.def, catchall: catchall }); inst.passthrough = () => inst.clone({ ...inst._zod.def, catchall: unknown() }); inst.loose = () => inst.clone({ ...inst._zod.def, catchall: unknown() }); inst.strict = () => inst.clone({ ...inst._zod.def, catchall: never() }); inst.strip = () => inst.clone({ ...inst._zod.def, catchall: undefined }); inst.extend = (incoming) => { return extend(inst, incoming); }; inst.safeExtend = (incoming) => { return safeExtend(inst, incoming); }; inst.merge = (other) => merge(inst, other); inst.pick = (mask) => pick(inst, mask); inst.omit = (mask) => omit(inst, mask); inst.partial = (...args) => partial(ZodOptional, inst, args[0]); inst.required = (...args) => required(ZodNonOptional, inst, args[0]); }); function object(shape, params) { const def = { type: "object", shape: shape ?? {}, ...normalizeParams(params), }; return new ZodObject(def); } const ZodUnion = /*@__PURE__*/ $constructor("ZodUnion", (inst, def) => { $ZodUnion.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => unionProcessor(inst, ctx, json, params); inst.options = def.options; }); function union(options, params) { return new ZodUnion({ type: "union", options: options, ...normalizeParams(params), }); } const ZodIntersection = /*@__PURE__*/ $constructor("ZodIntersection", (inst, def) => { $ZodIntersection.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => intersectionProcessor(inst, ctx, json, params); }); function intersection(left, right) { return new ZodIntersection({ type: "intersection", left: left, right: right, }); } const ZodTuple = /*@__PURE__*/ $constructor("ZodTuple", (inst, def) => { $ZodTuple.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => tupleProcessor(inst, ctx, json, params); inst.rest = (rest) => inst.clone({ ...inst._zod.def, rest: rest, }); }); function tuple(items, _paramsOrRest, _params) { const hasRest = _paramsOrRest instanceof $ZodType; const params = hasRest ? _params : _paramsOrRest; const rest = hasRest ? _paramsOrRest : null; return new ZodTuple({ type: "tuple", items: items, rest, ...normalizeParams(params), }); } const ZodEnum = /*@__PURE__*/ $constructor("ZodEnum", (inst, def) => { $ZodEnum.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => enumProcessor(inst, ctx, json); inst.enum = def.entries; inst.options = Object.values(def.entries); const keys = new Set(Object.keys(def.entries)); inst.extract = (values, params) => { const newEntries = {}; for (const value of values) { if (keys.has(value)) { newEntries[value] = def.entries[value]; } else throw new Error(`Key ${value} not found in enum`); } return new ZodEnum({ ...def, checks: [], ...normalizeParams(params), entries: newEntries, }); }; inst.exclude = (values, params) => { const newEntries = { ...def.entries }; for (const value of values) { if (keys.has(value)) { delete newEntries[value]; } else throw new Error(`Key ${value} not found in enum`); } return new ZodEnum({ ...def, checks: [], ...normalizeParams(params), entries: newEntries, }); }; }); function _enum(values, params) { const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values; return new ZodEnum({ type: "enum", entries, ...normalizeParams(params), }); } const ZodLiteral = /*@__PURE__*/ $constructor("ZodLiteral", (inst, def) => { $ZodLiteral.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => literalProcessor(inst, ctx, json); inst.values = new Set(def.values); Object.defineProperty(inst, "value", { get() { if (def.values.length > 1) { throw new Error("This schema contains multiple valid literal values. Use `.values` instead."); } return def.values[0]; }, }); }); function literal$1(value, params) { return new ZodLiteral({ type: "literal", values: Array.isArray(value) ? value : [value], ...normalizeParams(params), }); } const ZodTransform = /*@__PURE__*/ $constructor("ZodTransform", (inst, def) => { $ZodTransform.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => transformProcessor(inst, ctx); inst._zod.parse = (payload, _ctx) => { if (_ctx.direction === "backward") { throw new $ZodEncodeError(inst.constructor.name); } payload.addIssue = (issue$1) => { if (typeof issue$1 === "string") { payload.issues.push(issue(issue$1, payload.value, def)); } else { // for Zod 3 backwards compatibility const _issue = issue$1; if (_issue.fatal) _issue.continue = false; _issue.code ?? (_issue.code = "custom"); _issue.input ?? (_issue.input = payload.value); _issue.inst ?? (_issue.inst = inst); // _issue.continue ??= true; payload.issues.push(issue(_issue)); } }; const output = def.transform(payload.value, payload); if (output instanceof Promise) { return output.then((output) => { payload.value = output; return payload; }); } payload.value = output; return payload; }; }); function transform(fn) { return new ZodTransform({ type: "transform", transform: fn, }); } const ZodOptional = /*@__PURE__*/ $constructor("ZodOptional", (inst, def) => { $ZodOptional.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => optionalProcessor(inst, ctx, json, params); inst.unwrap = () => inst._zod.def.innerType; }); function optional(innerType) { return new ZodOptional({ type: "optional", innerType: innerType, }); } const ZodNullable = /*@__PURE__*/ $constructor("ZodNullable", (inst, def) => { $ZodNullable.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => nullableProcessor(inst, ctx, json, params); inst.unwrap = () => inst._zod.def.innerType; }); function nullable(innerType) { return new ZodNullable({ type: "nullable", innerType: innerType, }); } const ZodDefault = /*@__PURE__*/ $constructor("ZodDefault", (inst, def) => { $ZodDefault.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => defaultProcessor(inst, ctx, json, params); inst.unwrap = () => inst._zod.def.innerType; inst.removeDefault = inst.unwrap; }); function _default(innerType, defaultValue) { return new ZodDefault({ type: "default", innerType: innerType, get defaultValue() { return typeof defaultValue === "function" ? defaultValue() : shallowClone(defaultValue); }, }); } const ZodPrefault = /*@__PURE__*/ $constructor("ZodPrefault", (inst, def) => { $ZodPrefault.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => prefaultProcessor(inst, ctx, json, params); inst.unwrap = () => inst._zod.def.innerType; }); function prefault(innerType, defaultValue) { return new ZodPrefault({ type: "prefault", innerType: innerType, get defaultValue() { return typeof defaultValue === "function" ? defaultValue() : shallowClone(defaultValue); }, }); } const ZodNonOptional = /*@__PURE__*/ $constructor("ZodNonOptional", (inst, def) => { $ZodNonOptional.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => nonoptionalProcessor(inst, ctx, json, params); inst.unwrap = () => inst._zod.def.innerType; }); function nonoptional(innerType, params) { return new ZodNonOptional({ type: "nonoptional", innerType: innerType, ...normalizeParams(params), }); } const ZodCatch = /*@__PURE__*/ $constructor("ZodCatch", (inst, def) => { $ZodCatch.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => catchProcessor(inst, ctx, json, params); inst.unwrap = () => inst._zod.def.innerType; inst.removeCatch = inst.unwrap; }); function _catch(innerType, catchValue) { return new ZodCatch({ type: "catch", innerType: innerType, catchValue: (typeof catchValue === "function" ? catchValue : () => catchValue), }); } const ZodPipe = /*@__PURE__*/ $constructor("ZodPipe", (inst, def) => { $ZodPipe.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => pipeProcessor(inst, ctx, json, params); inst.in = def.in; inst.out = def.out; }); function pipe(in_, out) { return new ZodPipe({ type: "pipe", in: in_, out: out, // ...util.normalizeParams(params), }); } const ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => { $ZodReadonly.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => readonlyProcessor(inst, ctx, json, params); inst.unwrap = () => inst._zod.def.innerType; }); function readonly(innerType) { return new ZodReadonly({ type: "readonly", innerType: innerType, }); } const ZodFunction = /*@__PURE__*/ $constructor("ZodFunction", (inst, def) => { $ZodFunction.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => functionProcessor(inst, ctx); }); function _function(params) { return new ZodFunction({ type: "function", input: Array.isArray(params?.input) ? tuple(params?.input) : (array(unknown())), output: unknown(), }); } const ZodCustom = /*@__PURE__*/ $constructor("ZodCustom", (inst, def) => { $ZodCustom.init(inst, def); ZodType.init(inst, def); inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx); }); function custom(fn, _params) { return _custom(ZodCustom, fn ?? (() => true), _params); } function refine(fn, _params = {}) { return _refine(ZodCustom, fn, _params); } // superRefine function superRefine(fn) { return _superRefine(fn); } function _instanceof(cls, params = { error: `Input not instance of ${cls.name}`, }) { const inst = new ZodCustom({ type: "custom", check: "custom", fn: (data) => data instanceof cls, abort: true, ...normalizeParams(params), }); inst._zod.bag.Class = cls; return inst; } var p5$1 = { describe: { overloads: [ [ "String", "FALLBACK|LABEL?" ] ] }, describeElement: { overloads: [ [ "String", "String", "FALLBACK|LABEL?" ] ] }, textOutput: { overloads: [ [ "FALLBACK|LABEL?" ] ] }, gridOutput: { overloads: [ [ "FALLBACK|LABEL?" ] ] }, remove: { overloads: [ [ ] ] }, p5: { overloads: [ [ "Object", "String|HTMLElement" ] ] }, color: { overloads: [ [ "Number", "Number?" ], [ "Number", "Number", "Number", "Number?" ], [ "String" ], [ "Number[]" ], [ "p5.Color" ] ] }, red: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, green: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, blue: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, alpha: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, hue: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, saturation: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, brightness: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, lightness: { overloads: [ [ "p5.Color|Number[]|String" ] ] }, lerpColor: { overloads: [ [ "p5.Color", "p5.Color", "Number" ] ] }, paletteLerp: { overloads: [ [ "[p5.Color|String|Number|Number[], Number][]", "Number" ] ] }, beginClip: { overloads: [ [ "Object?" ] ] }, endClip: { overloads: [ [ ] ] }, clip: { overloads: [ [ "Function", "Object?" ] ] }, background: { overloads: [ [ "p5.Color" ], [ "String", "Number?" ], [ "Number", "Number?" ], [ "Number", "Number", "Number", "Number?" ], [ "Number[]" ], [ "p5.Image", "Number?" ] ] }, clear: { overloads: [ [ "Number?", "Number?", "Number?", "Number?" ], [ ] ] }, colorMode: { overloads: [ [ "RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH", "Number?" ], [ "RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH", "Number", "Number", "Number", "Number?" ], [ ] ] }, fill: { overloads: [ [ "Number", "Number", "Number", "Number?" ], [ "String" ], [ "Number", "Number?" ], [ "Number[]" ], [ "p5.Color" ] ] }, noFill: { overloads: [ [ ] ] }, noStroke: { overloads: [ [ ] ] }, stroke: { overloads: [ [ "Number", "Number", "Number", "Number?" ], [ "String" ], [ "Number", "Number?" ], [ "Number[]" ], [ "p5.Color" ] ] }, erase: { overloads: [ [ "Number?", "Number?" ] ] }, noErase: { overloads: [ [ ] ] }, blendMode: { overloads: [ [ "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT" ] ] }, print: { overloads: [ [ "Any" ], [ "String|Number|Array" ] ] }, cursor: { overloads: [ [ "ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String", "Number?", "Number?" ] ] }, frameRate: { overloads: [ [ "Number" ], [ ] ] }, getTargetFrameRate: { overloads: [ [ ] ] }, noCursor: { overloads: [ [ ] ] }, windowResized: { overloads: [ [ "Event?" ] ] }, fullscreen: { overloads: [ [ "Boolean?" ] ] }, pixelDensity: { overloads: [ [ "Number?" ], [ ] ] }, displayDensity: { overloads: [ [ ] ] }, getURL: { overloads: [ [ ] ] }, getURLPath: { overloads: [ [ ] ] }, getURLParams: { overloads: [ [ ] ] }, worldToScreen: { overloads: [ [ "Number|p5.Vector", "Number", "Number?" ] ] }, screenToWorld: { overloads: [ [ "Number|p5.Vector", "Number", "Number?" ] ] }, setup: { overloads: [ [ ] ] }, draw: { overloads: [ [ ] ] }, registerAddon: { overloads: [ [ "Function" ] ] }, createCanvas: { overloads: [ [ "Number?", "Number?", "P2D|WEBGL|P2DHDR|WEBGPU?", "HTMLCanvasElement?" ], [ "Number?", "Number?", "HTMLCanvasElement?" ] ] }, resizeCanvas: { overloads: [ [ "Number", "Number", "Boolean?" ] ] }, noCanvas: { overloads: [ [ ] ] }, createGraphics: { overloads: [ [ "Number", "Number", "P2D|WEBGL?", "HTMLCanvasElement?" ], [ "Number", "Number", "HTMLCanvasElement?" ] ] }, createFramebuffer: { overloads: [ [ "Object?" ] ] }, clearDepth: { overloads: [ [ "Number?" ] ] }, noLoop: { overloads: [ [ ] ] }, loop: { overloads: [ [ ] ] }, isLooping: { overloads: [ [ ] ] }, redraw: { overloads: [ [ "Integer?" ] ] }, applyMatrix: { overloads: [ [ "Number[]" ], [ "Number", "Number", "Number", "Number", "Number", "Number" ], [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number" ] ] }, resetMatrix: { overloads: [ [ ] ] }, rotate: { overloads: [ [ "Number", "p5.Vector|Number[]?" ] ] }, rotateX: { overloads: [ [ "Number" ] ] }, rotateY: { overloads: [ [ "Number" ] ] }, rotateZ: { overloads: [ [ "Number" ] ] }, scale: { overloads: [ [ "Number|p5.Vector|Number[]", "Number?", "Number?" ], [ "p5.Vector|Number[]" ] ] }, shearX: { overloads: [ [ "Number" ] ] }, shearY: { overloads: [ [ "Number" ] ] }, translate: { overloads: [ [ "Number", "Number", "Number?" ], [ "p5.Vector" ] ] }, push: { overloads: [ [ ] ] }, pop: { overloads: [ [ ] ] }, storeItem: { overloads: [ [ "String", "String|Number|Boolean|Object|Array" ] ] }, getItem: { overloads: [ [ "String" ] ] }, clearStorage: { overloads: [ [ ] ] }, removeItem: { overloads: [ [ "String" ] ] }, select: { overloads: [ [ "String", "String|p5.Element|HTMLElement?" ] ] }, selectAll: { overloads: [ [ "String", "String|p5.Element|HTMLElement?" ] ] }, createElement: { overloads: [ [ "String", "String?" ] ] }, removeElements: { overloads: [ [ ] ] }, addElement: { overloads: [ [ ] ] }, createDiv: { overloads: [ [ "String?" ] ] }, createP: { overloads: [ [ "String?" ] ] }, createSpan: { overloads: [ [ "String?" ] ] }, createImg: { overloads: [ [ "String", "String" ], [ "String", "String", "String?", "Function?" ] ] }, createA: { overloads: [ [ "String", "String", "String?" ] ] }, createSlider: { overloads: [ [ "Number", "Number", "Number?", "Number?" ] ] }, createButton: { overloads: [ [ "String", "String?" ] ] }, createCheckbox: { overloads: [ [ "String?", "Boolean?" ] ] }, createSelect: { overloads: [ [ "Boolean?" ], [ "Object" ] ] }, createRadio: { overloads: [ [ "Object?" ], [ "String?" ], [ ] ] }, createColorPicker: { overloads: [ [ "String|p5.Color?" ] ] }, createInput: { overloads: [ [ "String?", "String?" ], [ "String?" ] ] }, createFileInput: { overloads: [ [ "Function", "Boolean?" ] ] }, createVideo: { overloads: [ [ "String|String[]?", "Function?" ] ] }, createAudio: { overloads: [ [ "String|String[]?", "Function?" ] ] }, createCapture: { overloads: [ [ "AUDIO|VIDEO|Object?", "Object?", "Function?" ] ] }, setMoveThreshold: { overloads: [ [ "Number" ] ] }, setShakeThreshold: { overloads: [ [ "Number" ] ] }, deviceMoved: { overloads: [ [ ] ] }, deviceTurned: { overloads: [ [ ] ] }, deviceShaken: { overloads: [ [ ] ] }, keyPressed: { overloads: [ [ "KeyboardEvent?" ] ] }, keyReleased: { overloads: [ [ "KeyboardEvent?" ] ] }, keyTyped: { overloads: [ [ "KeyboardEvent?" ] ] }, keyIsDown: { overloads: [ [ "Number|String" ] ] }, mouseMoved: { overloads: [ [ "MouseEvent?" ] ] }, mouseDragged: { overloads: [ [ "MouseEvent?" ] ] }, mousePressed: { overloads: [ [ "MouseEvent?" ] ] }, mouseReleased: { overloads: [ [ "MouseEvent?" ] ] }, mouseClicked: { overloads: [ [ "MouseEvent?" ] ] }, doubleClicked: { overloads: [ [ "MouseEvent?" ] ] }, mouseWheel: { overloads: [ [ "WheelEvent?" ] ] }, requestPointerLock: { overloads: [ [ ] ] }, exitPointerLock: { overloads: [ [ ] ] }, createImage: { overloads: [ [ "Integer", "Integer" ] ] }, saveCanvas: { overloads: [ [ "p5.Framebuffer|p5.Element|HTMLCanvasElement", "String?", "String?" ], [ "String?", "String?" ] ] }, saveFrames: { overloads: [ [ "String", "String", "Number", "Number", "function(Array)?" ] ] }, loadImage: { overloads: [ [ "String|Request", "function(p5.Image)?", "function(Event)?" ] ] }, saveGif: { overloads: [ [ "String", "Number", "Object?" ] ] }, image: { overloads: [ [ "p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture|p5.Renderer|p5.Graphics", "Number", "Number", "Number?", "Number?" ], [ "p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture", "Number", "Number", "Number", "Number", "Number", "Number", "Number?", "Number?", "CONTAIN|COVER?", "LEFT|RIGHT|CENTER?", "TOP|BOTTOM|CENTER?" ] ] }, tint: { overloads: [ [ "Number", "Number", "Number", "Number?" ], [ "String" ], [ "Number", "Number?" ], [ "Number[]" ], [ "p5.Color" ] ] }, noTint: { overloads: [ [ ] ] }, imageMode: { overloads: [ [ "CORNER|CORNERS|CENTER" ] ] }, blend: { overloads: [ [ "p5.Image", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL" ], [ "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL" ] ] }, copy: { overloads: [ [ "p5.Image|p5.Element", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer" ], [ "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer" ] ] }, filter: { overloads: [ [ "THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR", "Number?", "Boolean?" ], [ "p5.Shader" ] ] }, get: { overloads: [ [ "Number", "Number", "Number", "Number" ], [ ], [ "Number", "Number" ] ] }, loadPixels: { overloads: [ [ ] ] }, set: { overloads: [ [ "Number", "Number", "Number|Number[]|Object" ] ] }, updatePixels: { overloads: [ [ "Number?", "Number?", "Number?", "Number?" ], [ ] ] }, loadJSON: { overloads: [ [ "String|Request", "Function?", "Function?" ] ] }, loadStrings: { overloads: [ [ "String|Request", "Function?", "Function?" ] ] }, loadTable: { overloads: [ [ "String|Request", "String?", "String?", "Function?", "Function?" ] ] }, loadXML: { overloads: [ [ "String|Request", "Function?", "Function?" ] ] }, loadBytes: { overloads: [ [ "String|Request", "Function?", "Function?" ] ] }, loadBlob: { overloads: [ [ "String|Request", "Function?", "Function?" ] ] }, httpGet: { overloads: [ [ "String|Request", "String?", "Function?", "Function?" ], [ "String|Request", "Function", "Function?" ] ] }, httpPost: { overloads: [ [ "String|Request", "Object|Boolean?", "String?", "Function?", "Function?" ], [ "String|Request", "Object|Boolean", "Function?", "Function?" ], [ "String|Request", "Function?", "Function?" ] ] }, httpDo: { overloads: [ [ "String|Request", "String?", "String?", "Object?", "Function?", "Function?" ], [ "String|Request", "Function?", "Function?" ] ] }, createWriter: { overloads: [ [ "String", "String?" ] ] }, write: { overloads: [ [ "String|Number|Array" ] ] }, close: { overloads: [ [ ] ] }, save: { overloads: [ [ "Object|String?", "String?", "Boolean|String?" ] ] }, saveJSON: { overloads: [ [ "Array|Object", "String", "Boolean?" ] ] }, saveStrings: { overloads: [ [ "String[]", "String", "String?", "Boolean?" ] ] }, saveTable: { overloads: [ [ "p5.Table", "String", "String?" ] ] }, setContent: { overloads: [ [ "String" ] ] }, abs: { overloads: [ [ "Number" ] ] }, ceil: { overloads: [ [ "Number" ] ] }, constrain: { overloads: [ [ "Number", "Number", "Number" ] ] }, dist: { overloads: [ [ "Number", "Number", "Number", "Number" ], [ "Number", "Number", "Number", "Number", "Number", "Number" ] ] }, exp: { overloads: [ [ "Number" ] ] }, floor: { overloads: [ [ "Number" ] ] }, lerp: { overloads: [ [ "Number", "Number", "Number" ] ] }, log: { overloads: [ [ "Number" ] ] }, mag: { overloads: [ [ "Number", "Number" ] ] }, map: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Boolean?" ] ] }, max: { overloads: [ [ "Number", "Number" ], [ "Number[]" ] ] }, min: { overloads: [ [ "Number", "Number" ], [ "Number[]" ] ] }, norm: { overloads: [ [ "Number", "Number", "Number" ] ] }, pow: { overloads: [ [ "Number", "Number" ] ] }, round: { overloads: [ [ "Number", "Number?" ] ] }, sq: { overloads: [ [ "Number" ] ] }, sqrt: { overloads: [ [ "Number" ] ] }, fract: { overloads: [ [ "Number" ] ] }, createVector: { overloads: [ [ "...Number[]" ] ] }, noise: { overloads: [ [ "Number", "Number?", "Number?" ] ] }, noiseDetail: { overloads: [ [ "Number" ], [ "Number", "Number" ] ] }, noiseSeed: { overloads: [ [ "Number" ] ] }, randomSeed: { overloads: [ [ "Number" ] ] }, random: { overloads: [ [ "Number?", "Number?" ], [ "Array" ] ] }, randomGaussian: { overloads: [ [ "Number?", "Number?" ] ] }, acos: { overloads: [ [ "Number" ] ] }, asin: { overloads: [ [ "Number" ] ] }, atan: { overloads: [ [ "Number" ] ] }, atan2: { overloads: [ [ "Number", "Number" ] ] }, cos: { overloads: [ [ "Number" ] ] }, sin: { overloads: [ [ "Number" ] ] }, tan: { overloads: [ [ "Number" ] ] }, degrees: { overloads: [ [ "Number" ] ] }, radians: { overloads: [ [ "Number" ] ] }, angleMode: { overloads: [ [ "RADIANS|DEGREES" ], [ ] ] }, arc: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number", "CHORD|PIE|OPEN?", "Integer?" ] ] }, ellipse: { overloads: [ [ "Number", "Number", "Number", "Number?" ], [ "Number", "Number", "Number", "Number", "Integer?" ] ] }, circle: { overloads: [ [ "Number", "Number", "Number" ] ] }, line: { overloads: [ [ "Number", "Number", "Number", "Number" ], [ "Number", "Number", "Number", "Number", "Number", "Number" ] ] }, point: { overloads: [ [ "Number", "Number", "Number?" ], [ "p5.Vector" ] ] }, quad: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Integer?", "Integer?" ], [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Integer?", "Integer?" ] ] }, rect: { overloads: [ [ "Number", "Number", "Number", "Number?", "Number?", "Number?", "Number?", "Number?" ], [ "Number", "Number", "Number", "Number", "Integer?", "Integer?" ] ] }, square: { overloads: [ [ "Number", "Number", "Number", "Number?", "Number?", "Number?", "Number?" ] ] }, triangle: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number" ] ] }, ellipseMode: { overloads: [ [ "CENTER|RADIUS|CORNER|CORNERS" ] ] }, noSmooth: { overloads: [ [ ] ] }, rectMode: { overloads: [ [ "CENTER|RADIUS|CORNER|CORNERS" ] ] }, smooth: { overloads: [ [ ] ] }, strokeCap: { overloads: [ [ "ROUND|SQUARE|PROJECT" ] ] }, strokeJoin: { overloads: [ [ "MITER|BEVEL|ROUND" ] ] }, strokeWeight: { overloads: [ [ "Number" ] ] }, bezier: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number" ], [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number" ] ] }, bezierPoint: { overloads: [ [ "Number", "Number", "Number", "Number", "Number" ] ] }, bezierTangent: { overloads: [ [ "Number", "Number", "Number", "Number", "Number" ] ] }, spline: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number" ], [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number" ] ] }, splinePoint: { overloads: [ [ "Number", "Number", "Number", "Number", "Number" ] ] }, splineTangent: { overloads: [ [ "Number", "Number", "Number", "Number", "Number" ] ] }, bezierOrder: { overloads: [ [ "Number" ], [ ] ] }, splineVertex: { overloads: [ [ "Number", "Number" ], [ "Number", "Number", "Number?" ], [ "Number", "Number", "Number?", "Number?" ], [ "Number", "Number", "Number", "Number?", "Number?" ] ] }, splineProperty: { overloads: [ [ "String", null ], [ "String" ] ] }, splineProperties: { overloads: [ [ "Object" ] ] }, vertex: { overloads: [ [ "Number", "Number" ], [ "Number", "Number", "Number?", "Number?" ], [ "Number", "Number", "Number", "Number?", "Number?" ] ] }, beginContour: { overloads: [ [ ] ] }, endContour: { overloads: [ [ "OPEN|CLOSE?" ] ] }, beginShape: { overloads: [ [ "POINTS|LINES|TRIANGLES|TRIANGLE_FAN|TRIANGLE_STRIP|QUADS|QUAD_STRIP|PATH?" ] ] }, bezierVertex: { overloads: [ [ "Number", "Number", "Number?", "Number?" ], [ "Number", "Number", "Number", "Number?", "Number?" ] ] }, endShape: { overloads: [ [ "CLOSE?", "Integer?" ] ] }, normal: { overloads: [ [ "p5.Vector" ], [ "Number", "Number", "Number" ] ] }, vertexProperty: { overloads: [ [ "String", "Number|Number[]" ] ] }, getWorldInputs: { overloads: [ [ "Function" ] ] }, getPixelInputs: { overloads: [ [ "Function" ] ] }, getFinalColor: { overloads: [ [ "Function" ] ] }, getColor: { overloads: [ [ "Function" ] ] }, getObjectInputs: { overloads: [ [ "Function" ] ] }, getCameraInputs: { overloads: [ [ "Function" ] ] }, loadFont: { overloads: [ [ "String", "String?", "Object?", "Function?", "Function?" ], [ "String", "Function?", "Function?" ] ] }, text: { overloads: [ [ "String|Object|Array|Number|Boolean", "Number", "Number", "Number?", "Number?" ] ] }, textAlign: { overloads: [ [ "LEFT|CENTER|RIGHT?", "TOP|BOTTOM|CENTER|BASELINE?" ] ] }, textAscent: { overloads: [ [ "String?" ] ] }, textDescent: { overloads: [ [ "String?" ] ] }, textLeading: { overloads: [ [ "Number?" ] ] }, textFont: { overloads: [ [ "p5.Font|String|Object?", "Number?" ] ] }, textSize: { overloads: [ [ "Number" ], [ ] ] }, textStyle: { overloads: [ [ "NORMAL|ITALIC|BOLD|BOLDITALIC" ], [ ] ] }, textWidth: { overloads: [ [ "String" ] ] }, textWrap: { overloads: [ [ "WORD|CHAR" ], [ ] ] }, textBounds: { overloads: [ [ "String", "Number", "Number", "Number?", "Number?" ] ] }, textDirection: { overloads: [ [ "String" ], [ ] ] }, textProperty: { overloads: [ [ "String", null ], [ "String" ] ] }, textProperties: { overloads: [ [ "Object" ], [ ] ] }, fontBounds: { overloads: [ [ "String", "Number", "Number", "Number?", "Number?" ] ] }, fontWidth: { overloads: [ [ "String" ] ] }, fontAscent: { overloads: [ [ ] ] }, fontDescent: { overloads: [ [ ] ] }, textWeight: { overloads: [ [ "Number" ], [ ] ] }, float: { overloads: [ [ "String" ], [ "String[]" ] ] }, int: { overloads: [ [ "String|Boolean|Number" ], [ "Array" ] ] }, str: { overloads: [ [ "String|Boolean|Number" ] ] }, boolean: { overloads: [ [ "String|Boolean|Number" ], [ "Array" ] ] }, byte: { overloads: [ [ "String|Boolean|Number" ], [ "Array" ] ] }, char: { overloads: [ [ "String|Number" ], [ "Array" ] ] }, unchar: { overloads: [ [ "String" ], [ "String[]" ] ] }, hex: { overloads: [ [ "Number", "Number?" ], [ "Number[]", "Number?" ] ] }, unhex: { overloads: [ [ "String" ], [ "String[]" ] ] }, day: { overloads: [ [ ] ] }, hour: { overloads: [ [ ] ] }, minute: { overloads: [ [ ] ] }, millis: { overloads: [ [ ] ] }, month: { overloads: [ [ ] ] }, second: { overloads: [ [ ] ] }, year: { overloads: [ [ ] ] }, nf: { overloads: [ [ "Number|String", "Integer|String?", "Integer|String?" ], [ "Number[]", "Integer|String?", "Integer|String?" ] ] }, nfc: { overloads: [ [ "Number|String", "Integer|String?" ], [ "Number[]", "Integer|String?" ] ] }, nfp: { overloads: [ [ "Number", "Integer?", "Integer?" ], [ "Number[]", "Integer?", "Integer?" ] ] }, nfs: { overloads: [ [ "Number", "Integer?", "Integer?" ], [ "Array", "Integer?", "Integer?" ] ] }, splitTokens: { overloads: [ [ "String", "String?" ] ] }, shuffle: { overloads: [ [ "Array", "Boolean?" ] ] }, strokeMode: { overloads: [ [ "String" ] ] }, buildGeometry: { overloads: [ [ "Function" ] ] }, freeGeometry: { overloads: [ [ "p5.Geometry" ] ] }, plane: { overloads: [ [ "Number?", "Number?", "Integer?", "Integer?" ] ] }, box: { overloads: [ [ "Number?", "Number?", "Number?", "Integer?", "Integer?" ] ] }, sphere: { overloads: [ [ "Number?", "Integer?", "Integer?" ] ] }, cylinder: { overloads: [ [ "Number?", "Number?", "Integer?", "Integer?", "Boolean?", "Boolean?" ] ] }, cone: { overloads: [ [ "Number?", "Number?", "Integer?", "Integer?", "Boolean?" ] ] }, ellipsoid: { overloads: [ [ "Number?", "Number?", "Number?", "Integer?", "Integer?" ] ] }, torus: { overloads: [ [ "Number?", "Number?", "Integer?", "Integer?" ] ] }, curveDetail: { overloads: [ [ "Number" ] ] }, orbitControl: { overloads: [ [ "Number?", "Number?", "Number?", "Object?" ] ] }, debugMode: { overloads: [ [ ], [ "GRID|AXES" ], [ "GRID|AXES", "Number?", "Number?", "Number?", "Number?", "Number?" ], [ "GRID|AXES", "Number?", "Number?", "Number?", "Number?" ], [ "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?" ] ] }, noDebugMode: { overloads: [ [ ] ] }, ambientLight: { overloads: [ [ "Number", "Number", "Number", "Number?" ], [ "Number", "Number?" ], [ "String" ], [ "Number[]" ], [ "p5.Color" ] ] }, specularColor: { overloads: [ [ "Number", "Number", "Number" ], [ "Number" ], [ "String" ], [ "Number[]" ], [ "p5.Color" ] ] }, directionalLight: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number" ], [ "Number", "Number", "Number", "p5.Vector" ], [ "p5.Color|Number[]|String", "Number", "Number", "Number" ], [ "p5.Color|Number[]|String", "p5.Vector" ] ] }, pointLight: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number" ], [ "Number", "Number", "Number", "p5.Vector" ], [ "p5.Color|Number[]|String", "Number", "Number", "Number" ], [ "p5.Color|Number[]|String", "p5.Vector" ] ] }, imageLight: { overloads: [ [ "p5.Image" ] ] }, panorama: { overloads: [ [ "p5.Image" ] ] }, lights: { overloads: [ [ ] ] }, lightFalloff: { overloads: [ [ "Number", "Number", "Number" ] ] }, spotLight: { overloads: [ [ "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number", "Number?", "Number?" ], [ "p5.Color|Number[]|String", "p5.Vector", "p5.Vector", "Number?", "Number?" ], [ "Number", "Number", "Number", "p5.Vector", "p5.Vector", "Number?", "Number?" ], [ "p5.Color|Number[]|String", "Number", "Number", "Number", "p5.Vector", "Number?", "Number?" ], [ "p5.Color|Number[]|String", "p5.Vector", "Number", "Number", "Number", "Number?", "Number?" ], [ "Number", "Number", "Number", "Number", "Number", "Number", "p5.Vector", "Number?", "Number?" ], [ "Number", "Number", "Number", "p5.Vector", "Number", "Number", "Number", "Number?", "Number?" ], [ "p5.Color|Number[]|String", "Number", "Number", "Number", "Number", "Number", "Number", "Number?", "Number?" ] ] }, noLights: { overloads: [ [ ] ] }, loadModel: { overloads: [ [ "String|Request", "String?", "Boolean?", "function(p5.Geometry)?", "function(Event)?" ], [ "String|Request", "String?", "function(p5.Geometry)?", "function(Event)?" ], [ "String|Request", "Object?" ] ] }, model: { overloads: [ [ "p5.Geometry", "Number?" ] ] }, createModel: { overloads: [ [ "String", "String?", "Boolean?", "function(p5.Geometry)?", "function(Event)?" ], [ "String", "String?", "function(p5.Geometry)?", "function(Event)?" ], [ "String", "String?", "Object?" ] ] }, loadShader: { overloads: [ [ "String|Request", "String|Request", "Function?", "Function?" ] ] }, createShader: { overloads: [ [ "String", "String", "Object?" ] ] }, loadFilterShader: { overloads: [ [ "String", "Function?", "Function?" ] ] }, buildFilterShader: { overloads: [ [ "Function" ], [ "Object" ] ] }, createFilterShader: { overloads: [ [ "String" ] ] }, shader: { overloads: [ [ "p5.Shader" ] ] }, strokeShader: { overloads: [ [ "p5.Shader" ] ] }, imageShader: { overloads: [ [ "p5.Shader" ] ] }, buildMaterialShader: { overloads: [ [ "Function" ], [ "Object" ] ] }, loadMaterialShader: { overloads: [ [ "String", "Function?", "Function?" ] ] }, baseMaterialShader: { overloads: [ [ ] ] }, baseFilterShader: { overloads: [ [ ] ] }, buildNormalShader: { overloads: [ [ "Function" ], [ "Object" ] ] }, loadNormalShader: { overloads: [ [ "String", "Function?", "Function?" ] ] }, baseNormalShader: { overloads: [ [ ] ] }, buildColorShader: { overloads: [ [ "Function" ], [ "Object" ] ] }, loadColorShader: { overloads: [ [ "String", "Function?", "Function?" ] ] }, baseColorShader: { overloads: [ [ ] ] }, buildStrokeShader: { overloads: [ [ "Function" ], [ "Object" ] ] }, loadStrokeShader: { overloads: [ [ "String", "Function?", "Function?" ] ] }, baseStrokeShader: { overloads: [ [ ] ] }, resetShader: { overloads: [ [ ] ] }, texture: { overloads: [ [ "p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture" ] ] }, textureMode: { overloads: [ [ "IMAGE|NORMAL" ] ] }, textureWrap: { overloads: [ [ "CLAMP|REPEAT|MIRROR", "CLAMP|REPEAT|MIRROR?" ] ] }, normalMaterial: { overloads: [ [ ] ] }, ambientMaterial: { overloads: [ [ "Number", "Number", "Number" ], [ "Number" ], [ "p5.Color|Number[]|String" ] ] }, emissiveMaterial: { overloads: [ [ "Number", "Number", "Number", "Number?" ], [ "Number" ], [ "p5.Color|Number[]|String" ] ] }, specularMaterial: { overloads: [ [ "Number", "Number?" ], [ "Number", "Number", "Number", "Number?" ], [ "p5.Color|Number[]|String" ] ] }, shininess: { overloads: [ [ "Number" ] ] }, metalness: { overloads: [ [ "Number" ] ] }, roll: { overloads: [ [ "Number" ] ] }, camera: { overloads: [ [ "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?" ] ] }, perspective: { overloads: [ [ "Number?", "Number?", "Number?", "Number?" ] ] }, linePerspective: { overloads: [ [ "Boolean" ], [ ] ] }, ortho: { overloads: [ [ "Number?", "Number?", "Number?", "Number?", "Number?", "Number?" ] ] }, frustum: { overloads: [ [ "Number?", "Number?", "Number?", "Number?", "Number?", "Number?" ] ] }, createCamera: { overloads: [ [ ] ] }, setCamera: { overloads: [ [ "p5.Camera" ] ] }, saveObj: { overloads: [ [ "String?" ] ] }, saveStl: { overloads: [ [ "String?", "Object?" ] ] }, fromAxisAngle: { overloads: [ [ "Number?", "Number?", "Number?", "Number?" ] ] }, mult: { overloads: [ [ "p5.Quat?" ] ] }, rotateBy: { overloads: [ [ "p5.Quat?" ] ] }, setAttributes: { overloads: [ [ "String", "Boolean" ], [ "Object" ] ] } }; var dataDoc = { p5: p5$1, "p5.Color": { toString: { overloads: [ [ "String?" ] ] }, contrast: { overloads: [ [ "Color" ] ] }, setRed: { overloads: [ [ "Number" ] ] }, setGreen: { overloads: [ [ "Number" ] ] }, setBlue: { overloads: [ [ "Number" ] ] }, setAlpha: { overloads: [ [ "Number" ] ] } }, "p5.Graphics": { reset: { overloads: [ [ ] ] }, remove: { overloads: [ [ ] ] }, createFramebuffer: { overloads: [ [ "Object?" ] ] } }, "p5.Element": { remove: { overloads: [ [ ] ] }, parent: { overloads: [ [ "String|p5.Element|Object" ], [ ] ] }, child: { overloads: [ [ ], [ "String|p5.Element?" ] ] }, html: { overloads: [ [ ], [ "String?", "Boolean?" ] ] }, id: { overloads: [ [ "String" ], [ ] ] }, "class": { overloads: [ [ "String" ], [ ] ] }, addClass: { overloads: [ [ "String" ] ] }, removeClass: { overloads: [ [ "String" ] ] }, hasClass: { overloads: [ [ null ] ] }, toggleClass: { overloads: [ [ null ] ] }, center: { overloads: [ [ "String?" ] ] }, position: { overloads: [ [ ], [ "Number?", "Number?", "String?" ] ] }, show: { overloads: [ [ ] ] }, hide: { overloads: [ [ ] ] }, size: { overloads: [ [ ], [ "Number|AUTO?", "Number|AUTO?" ] ] }, style: { overloads: [ [ "String" ], [ "String", "String|p5.Color" ] ] }, attribute: { overloads: [ [ ], [ "String", "String" ] ] }, removeAttribute: { overloads: [ [ "String" ] ] }, value: { overloads: [ [ ], [ "String|Number" ] ] }, mousePressed: { overloads: [ [ "Function|Boolean" ] ] }, doubleClicked: { overloads: [ [ "Function|Boolean" ] ] }, mouseWheel: { overloads: [ [ "Function|Boolean" ] ] }, mouseReleased: { overloads: [ [ "Function|Boolean" ] ] }, mouseClicked: { overloads: [ [ "Function|Boolean" ] ] }, mouseMoved: { overloads: [ [ "Function|Boolean" ] ] }, mouseOver: { overloads: [ [ "Function|Boolean" ] ] }, mouseOut: { overloads: [ [ "Function|Boolean" ] ] }, dragOver: { overloads: [ [ "Function|Boolean" ] ] }, dragLeave: { overloads: [ [ "Function|Boolean" ] ] }, changed: { overloads: [ [ "Function|Boolean" ] ] }, input: { overloads: [ [ "Function|Boolean" ] ] }, drop: { overloads: [ [ "Function", "Function?" ] ] }, draggable: { overloads: [ [ "p5.Element?" ] ] } }, "p5.MediaElement": { play: { overloads: [ [ ] ] }, stop: { overloads: [ [ ] ] }, pause: { overloads: [ [ ] ] }, loop: { overloads: [ [ ] ] }, noLoop: { overloads: [ [ ] ] }, autoplay: { overloads: [ [ "Boolean?" ] ] }, volume: { overloads: [ [ ], [ "Number" ] ] }, speed: { overloads: [ [ ], [ "Number" ] ] }, time: { overloads: [ [ "Number?" ] ] }, duration: { overloads: [ [ ] ] }, onended: { overloads: [ [ "Function" ] ] }, connect: { overloads: [ [ "AudioNode|Object" ] ] }, disconnect: { overloads: [ [ ] ] }, showControls: { overloads: [ [ ] ] }, hideControls: { overloads: [ [ ] ] }, addCue: { overloads: [ [ "Number", "Function", "Object?" ] ] }, removeCue: { overloads: [ [ "Number" ] ] }, clearCues: { overloads: [ [ ] ] } }, "p5.Image": { pixelDensity: { overloads: [ [ "Number?" ] ] }, loadPixels: { overloads: [ [ ] ] }, updatePixels: { overloads: [ [ "Integer?", "Integer?", "Integer?", "Integer?" ] ] }, get: { overloads: [ [ "Number", "Number", "Number", "Number" ], [ ], [ "Number", "Number" ] ] }, set: { overloads: [ [ "Number", "Number", "Number|Number[]|Object" ] ] }, resize: { overloads: [ [ "Number", "Number" ] ] }, copy: { overloads: [ [ "p5.Image|p5.Element", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer" ], [ "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer" ] ] }, mask: { overloads: [ [ "p5.Image" ] ] }, filter: { overloads: [ [ "THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|ERODE|DILATE|BLUR", "Number?" ] ] }, blend: { overloads: [ [ "p5.Image", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL" ], [ "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "Integer", "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL" ] ] }, save: { overloads: [ [ "String", "String?" ] ] }, reset: { overloads: [ [ ] ] }, getCurrentFrame: { overloads: [ [ ] ] }, setFrame: { overloads: [ [ "Number" ] ] }, numFrames: { overloads: [ [ ] ] }, play: { overloads: [ [ ] ] }, pause: { overloads: [ [ ] ] }, delay: { overloads: [ [ "Number", "Number?" ] ] } }, "p5.Table": { addRow: { overloads: [ [ "p5.TableRow?" ] ] }, removeRow: { overloads: [ [ "Integer" ] ] }, getRow: { overloads: [ [ "Integer" ] ] }, getRows: { overloads: [ [ ] ] }, findRow: { overloads: [ [ "String", "Integer|String" ] ] }, findRows: { overloads: [ [ "String", "Integer|String" ] ] }, matchRow: { overloads: [ [ "String|RegExp", "String|Integer" ] ] }, matchRows: { overloads: [ [ "String", "String|Integer?" ] ] }, getColumn: { overloads: [ [ "String|Number" ] ] }, clearRows: { overloads: [ [ ] ] }, addColumn: { overloads: [ [ "String?" ] ] }, getColumnCount: { overloads: [ [ ] ] }, getRowCount: { overloads: [ [ ] ] }, removeTokens: { overloads: [ [ "String", "String|Integer?" ] ] }, trim: { overloads: [ [ "String|Integer?" ] ] }, removeColumn: { overloads: [ [ "String|Integer" ] ] }, set: { overloads: [ [ "Integer", "String|Integer", "String|Number" ] ] }, setNum: { overloads: [ [ "Integer", "String|Integer", "Number" ] ] }, setString: { overloads: [ [ "Integer", "String|Integer", "String" ] ] }, get: { overloads: [ [ "Integer", "String|Integer" ] ] }, getNum: { overloads: [ [ "Integer", "String|Integer" ] ] }, getString: { overloads: [ [ "Integer", "String|Integer" ] ] }, getObject: { overloads: [ [ "String?" ] ] }, getArray: { overloads: [ [ ] ] } }, "p5.TableRow": { set: { overloads: [ [ "String|Integer", "String|Number" ] ] }, setNum: { overloads: [ [ "String|Integer", "Number|String" ] ] }, setString: { overloads: [ [ "String|Integer", "String|Number|Boolean|Object" ] ] }, get: { overloads: [ [ "String|Integer" ] ] }, getNum: { overloads: [ [ "String|Integer" ] ] }, getString: { overloads: [ [ "String|Integer" ] ] } }, "p5.XML": { getParent: { overloads: [ [ ] ] }, getName: { overloads: [ [ ] ] }, setName: { overloads: [ [ "String" ] ] }, hasChildren: { overloads: [ [ ] ] }, listChildren: { overloads: [ [ ] ] }, getChildren: { overloads: [ [ "String?" ] ] }, getChild: { overloads: [ [ "String|Integer" ] ] }, addChild: { overloads: [ [ "p5.XML" ] ] }, removeChild: { overloads: [ [ "String|Integer" ] ] }, getAttributeCount: { overloads: [ [ ] ] }, listAttributes: { overloads: [ [ ] ] }, hasAttribute: { overloads: [ [ "String" ] ] }, getNum: { overloads: [ [ "String", "Number?" ] ] }, getString: { overloads: [ [ "String", "Number?" ] ] }, setAttribute: { overloads: [ [ "String", "Number|String|Boolean" ] ] }, getContent: { overloads: [ [ "String?" ] ] }, serialize: { overloads: [ [ ] ] } }, "p5.Vector": { getValue: { overloads: [ [ "Number" ] ] }, setValue: { overloads: [ [ "Number", "Number" ] ] }, set: { overloads: [ [ "Number?", "Number?", "Number?" ], [ "p5.Vector|Number[]" ] ] }, copy: { overloads: [ [ ] ] }, add: { overloads: [ [ "Number|Array", "Number?", "Number?" ], [ "p5.Vector|Number[]" ] ] }, rem: { overloads: [ [ "Number", "Number", "Number" ], [ "p5.Vector|Number[]" ] ] }, sub: { overloads: [ [ "Number", "Number?", "Number?" ], [ "p5.Vector|Number[]" ] ] }, mult: { overloads: [ [ "Number" ], [ "Number", "Number", "Number?" ], [ "Number[]" ], [ "p5.Vector" ] ] }, div: { overloads: [ [ "Number" ], [ "Number", "Number", "Number?" ], [ "Number[]" ], [ "p5.Vector" ] ] }, mag: { overloads: [ [ ] ] }, magSq: { overloads: [ [ ] ] }, dot: { overloads: [ [ "Number", "Number?", "Number?" ], [ "p5.Vector" ] ] }, cross: { overloads: [ [ "p5.Vector" ] ] }, dist: { overloads: [ [ "p5.Vector" ] ] }, normalize: { overloads: [ [ ] ] }, limit: { overloads: [ [ "Number" ] ] }, setMag: { overloads: [ [ "Number" ] ] }, heading: { overloads: [ [ ] ] }, setHeading: { overloads: [ [ "Number" ] ] }, rotate: { overloads: [ [ "Number" ] ] }, angleBetween: { overloads: [ [ "p5.Vector" ] ] }, lerp: { overloads: [ [ "Number", "Number", "Number", "Number" ], [ "p5.Vector", "Number" ] ] }, slerp: { overloads: [ [ "p5.Vector", "Number" ] ] }, reflect: { overloads: [ [ "p5.Vector" ] ] }, array: { overloads: [ [ ] ] }, equals: { overloads: [ [ "Number?", "Number?", "Number?" ], [ "p5.Vector|Array" ] ] }, clampToZero: { overloads: [ [ ] ] }, fromAngle: { overloads: [ [ "Number", "Number?" ] ] }, fromAngles: { overloads: [ [ "Number", "Number", "Number?" ] ] }, random2D: { overloads: [ [ ] ] }, random3D: { overloads: [ [ ] ] } }, "p5.Font": { textToPaths: { overloads: [ [ "String", "Number", "Number", "Number?", "Number?" ] ] }, textToPoints: { overloads: [ [ "String", "Number", "Number", "Object?" ] ] }, textToContours: { overloads: [ [ "String", "Number", "Number", "Object?" ] ] }, textToModel: { overloads: [ [ "String", "Number", "Number", "Number", "Number", "Object?" ] ] } }, "p5.Camera": { perspective: { overloads: [ [ "Number?", "Number?", "Number?", "Number?" ] ] }, ortho: { overloads: [ [ "Number?", "Number?", "Number?", "Number?", "Number?", "Number?" ] ] }, frustum: { overloads: [ [ "Number?", "Number?", "Number?", "Number?", "Number?", "Number?" ] ] }, pan: { overloads: [ [ "Number" ] ] }, tilt: { overloads: [ [ "Number" ] ] }, lookAt: { overloads: [ [ "Number", "Number", "Number" ] ] }, camera: { overloads: [ [ "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?", "Number?" ] ] }, move: { overloads: [ [ "Number", "Number", "Number" ] ] }, setPosition: { overloads: [ [ "Number", "Number", "Number" ] ] }, set: { overloads: [ [ "p5.Camera" ] ] }, slerp: { overloads: [ [ "p5.Camera", "p5.Camera", "Number" ] ] } }, "p5.Framebuffer": { resize: { overloads: [ [ "Number", "Number" ] ] }, pixelDensity: { overloads: [ [ "Number?" ] ] }, autoSized: { overloads: [ [ "Boolean?" ] ] }, createCamera: { overloads: [ [ ] ] }, remove: { overloads: [ [ ] ] }, begin: { overloads: [ [ ] ] }, end: { overloads: [ [ ] ] }, draw: { overloads: [ [ "Function" ] ] }, loadPixels: { overloads: [ [ ] ] }, get: { overloads: [ [ "Number", "Number", "Number", "Number" ], [ ], [ "Number", "Number" ] ] } }, "p5.Geometry": { calculateBoundingBox: { overloads: [ [ ] ] }, clearColors: { overloads: [ [ ] ] }, flipU: { overloads: [ [ ] ] }, computeFaces: { overloads: [ [ ] ] }, computeNormals: { overloads: [ [ "FLAT|SMOOTH?", "Object?" ] ] }, makeEdgesFromFaces: { overloads: [ [ ] ] }, normalize: { overloads: [ [ ] ] }, vertexProperty: { overloads: [ [ "String", "Number|Number[]", "Number?" ] ] }, flipV: { overloads: [ [ ] ] } }, "p5.Shader": { version: { overloads: [ [ ] ] }, inspectHooks: { overloads: [ [ ] ] }, modify: { overloads: [ [ "Function", "Object?" ], [ "Object?" ] ] }, copyToContext: { overloads: [ [ "p5|p5.Graphics" ] ] }, setUniform: { overloads: [ [ "String", "Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture" ] ] } } }; /** * @for p5 * @requires core */ function validateParams(p5, fn, lifecycles) { // Cache for Zod schemas let schemaRegistry = new Map(); // Mapping names of p5 types to their constructor functions. // p5Constructors: // - Color: f() // - Graphics: f() // - Vector: f() // and so on. // const p5Constructors = {}; // NOTE: This is a tempt fix for unit test but is not correct // Attaced constructors are `undefined` const p5Constructors = Object.keys(p5).reduce((acc, val) => { if ( val.match(/^[A-Z]/) && // Starts with a capital !val.match(/^[A-Z][A-Z0-9]*$/) && // Is not an all caps constant p5[val] instanceof Function // Is a function ) { acc[val] = p5[val]; } return acc; }, {}); function loadP5Constructors() { // Make a list of all p5 classes to be used for argument validation // This must be done only when everything has loaded otherwise we get // an empty array for (let key of Object.keys(p5)) { // Get a list of all constructors in p5. They are functions whose names // start with a capital letter if (typeof p5[key] === 'function' && key[0] !== key[0].toLowerCase()) { p5Constructors[key] = p5[key]; } } } // `constantsMap` maps constants to their values, e.g. // { // ADD: 'lighter', // ALT: 18, // ARROW: 'default', // AUTO: 'auto', // ... // } const constantsMap = {}; for (const [key, value] of Object.entries(constants)) { constantsMap[key] = value; } // Start initializing `schemaMap` with primitive types. `schemaMap` will // eventually contain both primitive types and web API objects. const schemaMap = { 'Any': any(), 'Array': array(any()), 'Boolean': boolean(), 'Function': _function(), 'Integer': number().int(), 'Number': number(), 'Object': object({}), 'String': string() }; const webAPIObjects = [ 'AudioNode', 'HTMLCanvasElement', 'HTMLElement', 'KeyboardEvent', 'MouseEvent', 'RegExp', 'TouchEvent', 'UIEvent', 'WheelEvent' ]; function generateWebAPISchemas(apiObjects) { return apiObjects.reduce((acc, obj) => { acc[obj] = custom(data => data instanceof globalThis[obj], { message: `Expected a ${obj}` }); return acc; }, {}); } const webAPISchemas = generateWebAPISchemas(webAPIObjects); // Add web API schemas to the schema map. Object.assign(schemaMap, webAPISchemas); // For mapping 0-indexed parameters to their ordinal representation, e.g. // "first" for 0, "second" for 1, "third" for 2, etc. const ordinals = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth']; function extractFuncNameAndClass(func) { const ichDot = func.lastIndexOf('.'); const funcName = func.slice(ichDot + 1); const funcClass = func.slice(0, ichDot !== -1 ? ichDot : 0) || 'p5'; return { funcName, funcClass }; } function validBracketNesting(type) { let level = 0; for (let i = 0; i < type.length; i++) { if (type[i] === '[') { level++; } else if (type[i] === ']') { level--; if (level < 0) return false; } } return level === 0; } /** * This is a helper function that generates Zod schemas for a function based on * the parameter data from `docs/parameterData.json`. * * Example parameter data for function `background`: * "background": { * "overloads": [ * ["p5.Color"], * ["String", "Number?"], * ["Number", "Number?"], * ["Number", "Number", "Number", "Number?"], * ["Number[]"], * ["p5.Image", "Number?"] * ] * } * Where each array in `overloads` represents a set of valid overloaded * parameters, and `?` is a shorthand for `Optional`. * * @method generateZodSchemasForFunc * @private * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add` * @returns {z.ZodSchema} Zod schema */ const generateZodSchemasForFunc = function (func) { const { funcName, funcClass } = extractFuncNameAndClass(func); let funcInfo = dataDoc[funcClass][funcName]; if(!funcInfo) return; let overloads = []; if (funcInfo.hasOwnProperty('overloads')) { overloads = funcInfo.overloads; } // Returns a schema for a single type, i.e. z.boolean() for `boolean`. const generateTypeSchema = baseType => { if (!baseType) return any(); let typeSchema; // Check for constants. Note that because we're ultimately interested in the value of // the constant, mapping constants to their values via `constantsMap` is // necessary. if (baseType in constantsMap) { typeSchema = literal$1(constantsMap[baseType]); } // Some more constants are attached directly to p5.prototype, e.g. by addons: else if (baseType.match(/^[A-Z][A-Z0-9]*$/) && baseType in fn) { typeSchema = literal$1(fn[baseType]); } // Function types else if (baseType.startsWith('function')) { typeSchema = _function(); } // All p5 objects start with `p5` in the documentation, i.e. `p5.Camera`. else if (/^p5\.[a-zA-Z0-9]+$/.exec(baseType) || baseType === 'p5') { const className = baseType.substring(baseType.indexOf('.') + 1); typeSchema = _instanceof(p5Constructors[className]); } // For primitive types and web API objects. else if (schemaMap[baseType]) { typeSchema = schemaMap[baseType]; } // Tuple types else if ( baseType.startsWith('[') && baseType.endsWith(']') && validBracketNesting(baseType.slice(1, -1)) ) { typeSchema = tuple( baseType .slice(1, -1) .split(/, */g) .map(entry => generateTypeSchema(entry)) ); } // JavaScript classes, e.g. Request else if (baseType.match(/^[A-Z]/) && baseType in window) { typeSchema = _instanceof(window[baseType]); } // Generate a schema for a single parameter that can be of multiple // types / constants, i.e. `String|Number|Array`. // // Here, z.union() is used over z.enum() (which seems more intuitive) for // constants for the following reasons: // 1) z.enum() only allows a fixed set of allowable string values. However, // our constants sometimes have numeric or non-primitive values. // 2) In some cases, the type can be constants or strings, making z.enum() // insufficient for the use case. else if (baseType.includes('|') && baseType.split('|').every(t => validBracketNesting(t))) { const types = baseType.split('|'); typeSchema = union(types .map(t => generateTypeSchema(t)) .filter(s => s !== undefined)); } else if (baseType.endsWith('[]')) { typeSchema = array(generateTypeSchema(baseType.slice(0, -2))); } else { throw new Error(`Unsupported type '${baseType}' in parameter validation. Please report this issue.`); } return typeSchema; }; // Generate a schema for a single parameter. In the case where a parameter can // be of multiple types, `generateTypeSchema` is called for each type. const generateParamSchema = param => { const isOptional = param?.endsWith('?'); param = param?.replace(/\?$/, ''); const isRest = param?.startsWith('...') && param?.endsWith('[]'); param = param?.replace(/^\.\.\.(.+)\[\]$/, '$1'); let schema = generateTypeSchema(param); // Fallback to z.custom() because function types are no longer // returns a Zod schema. if (schema.def.type === 'function') { schema = custom(val => val instanceof Function); } if (isOptional) { schema = schema.optional(); } return { schema, rest: isRest }; }; // Note that in Zod, `optional()` only checks for undefined, not the absence // of value. // // Let's say we have a function with 3 parameters, and the last one is // optional, i.e. func(a, b, c?). If we only have a z.tuple() for the // parameters, where the third schema is optional, then we will only be able // to validate func(10, 10, undefined), but not func(10, 10), which is // a completely valid call. // // Therefore, on top of using `optional()`, we also have to generate parameter // combinations that are valid for all numbers of parameters. const generateOverloadCombinations = params => { // No optional parameters, return the original parameter list right away. if (!params.some(p => p?.endsWith('?'))) { return [params]; } const requiredParamsCount = params.filter(p => p === null || !p.endsWith('?')).length; const result = []; for (let i = requiredParamsCount; i <= params.length; i++) { result.push(params.slice(0, i)); } return result; }; // Generate schemas for each function overload and merge them const overloadSchemas = overloads.flatMap(overload => { const combinations = generateOverloadCombinations(overload); return combinations.map(combo => { const params = combo .map(p => generateParamSchema(p)) .filter(s => s.schema !== undefined); let rest; if (params.at(-1)?.rest) { rest = params.pop(); } let combined = tuple(params.map(s => s.schema)); if (rest) { combined = combined.rest(rest.schema); } return combined; }); }); return overloadSchemas.length === 1 ? overloadSchemas[0] : union(overloadSchemas); }; /** * Finds the closest schema to the input arguments. * * This is a helper function that identifies the closest schema to the input * arguments, in the case of an initial validation error. We will then use the * closest schema to generate a friendly error message. * * @private * @param {z.ZodSchema} schema - Zod schema. * @param {Array} args - User input arguments. * @returns {z.ZodSchema} Closest schema matching the input arguments. */ const findClosestSchema = function (schema, args) { if (!(schema instanceof ZodUnion)) { return schema; } // Helper function that scores how close the input arguments are to a schema. // Lower score means closer match. const scoreSchema = schema => { let score = Infinity; if (!(schema instanceof ZodTuple)) { console.warn('Schema below is not a tuple: '); printZodSchema(schema); return score; } const numArgs = args.length; const schemaItems = schema.def.items; const numSchemaItems = schemaItems.length; const numRequiredSchemaItems = schemaItems .filter(item => !item.isOptional()) .length; if (numArgs >= numRequiredSchemaItems && numArgs <= numSchemaItems) { score = 0; } // Here, give more weight to mismatch in number of arguments. // // For example, color() can either take [Number, Number?] or // [Number, Number, Number, Number?] as list of parameters. // If the user passed in 3 arguments, [10, undefined, undefined], it's // more than likely that they intended to pass in 3 arguments, but the // last two arguments are invalid. // // If there's no bias towards matching the number of arguments, the error // message will show that we're expecting at most 2 arguments, but more // are received. else { score = Math.abs( numArgs < numRequiredSchemaItems ? numRequiredSchemaItems - numArgs : numArgs - numSchemaItems ) * 4; } for (let i = 0; i < Math.min(schemaItems.length, args.length); i++) { const paramSchema = schemaItems[i]; const arg = args[i]; if (!paramSchema.safeParse(arg).success) score++; } return score; }; // Default to the first schema, so that we are guaranteed to return a result. let closestSchema = schema.def.options[0]; // We want to return the schema with the lowest score. let bestScore = Infinity; const schemaUnion = schema.def.options; schemaUnion.forEach(schema => { const score = scoreSchema(schema); if (score < bestScore) { closestSchema = schema; bestScore = score; } }); return closestSchema; }; /** * Prints a friendly error message after parameter validation, if validation * has failed. * * @method _friendlyParamError * @private * @param {z.ZodError} zodErrorObj - The Zod error object containing validation errors. * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add` * @returns {String} The friendly error message. */ const friendlyParamError = function (zodErrorObj, func, args) { let message = '🌸 p5.js says: '; let isVersionError = false; // The `zodErrorObj` might contain multiple errors of equal importance // (after scoring the schema closeness in `findClosestSchema`). Here, we // always print the first error so that user can work through the errors // one by one. let currentError = zodErrorObj.issues[0]; // Helper function to build a type mismatch message. const buildTypeMismatchMessage = (actualType, expectedTypeStr, position) => { const positionStr = position ? `at the ${ordinals[position]} parameter` : ''; const actualTypeStr = actualType ? `, but received ${actualType}` : ''; return `Expected ${expectedTypeStr} ${positionStr}${actualTypeStr}`; }; // Union errors occur when a parameter can be of multiple types but is not // of any of them. In this case, aggregate all possible types and print // a friendly error message that indicates what the expected types are at // which position (position is not 0-indexed, for accessibility reasons). const processUnionError = error => { const expectedTypes = new Set(); let actualType; error.errors.forEach(err => { const issue = err[0]; if (issue) { if (!actualType) { actualType = issue.message; } if (issue.code === 'invalid_type') { actualType = issue.message.split(', received ')[1]; expectedTypes.add(issue.expected); } // The case for constants. Since we don't want to print out the actual // constant values in the error message, the error message will // direct users to the documentation. else if (issue.code === 'invalid_value') { expectedTypes.add('constant (please refer to documentation for allowed values)'); actualType = args[error.path[0]]; } else if (issue.code === 'custom') { const match = issue.message.match(/Input not instance of (\w+)/); if (match) expectedTypes.add(match[1]); actualType = undefined; } } }); if (expectedTypes.size > 0) { if (error.path?.length > 0 && args[error.path[0]] instanceof Promise) { message += 'Did you mean to put `await` before a loading function? ' + 'An unexpected Promise was found. '; isVersionError = true; } const expectedTypesStr = Array.from(expectedTypes).join(' or '); const position = error.path.join('.'); message += buildTypeMismatchMessage( actualType, expectedTypesStr, position ); } return message; }; switch (currentError.code) { case 'invalid_union': { processUnionError(currentError); break; } case 'too_small': { const minArgs = currentError.minimum; message += `Expected at least ${minArgs} argument${minArgs > 1 ? 's' : ''}, but received fewer`; break; } case 'invalid_type': { message += buildTypeMismatchMessage(currentError.message.split(', received ')[1], currentError.expected, currentError.path.join('.')); break; } case 'too_big': { const maxArgs = currentError.maximum; message += `Expected at most ${maxArgs} argument${maxArgs > 1 ? 's' : ''}, but received more`; break; } default: { console.log('Zod error object', currentError); } } // Let the user know which function is generating the error. message += ` in ${func}().`; // Generates a link to the documentation based on the given function name. // TODO: Check if the link is reachable before appending it to the error // message. const generateDocumentationLink = func => { const { funcName, funcClass } = extractFuncNameAndClass(func); const p5BaseUrl = 'https://p5js.org/reference'; const url = `${p5BaseUrl}/${funcClass}/${funcName}`; return url; }; if (currentError.code === 'too_big' || currentError.code === 'too_small') { const documentationLink = generateDocumentationLink(func); message += ` For more information, see ${documentationLink}.`; } if (isVersionError) { p5._error(this, message); } else { console.log(message); } return message; }; /** * Runs parameter validation by matching the input parameters to Zod schemas * generated from the parameter data from `docs/parameterData.json`. * * @private * @param {String} func - Name of the function. * @param {Array} args - User input arguments. * @returns {Object} The validation result. * @returns {Boolean} result.success - Whether the validation was successful. * @returns {any} [result.data] - The parsed data if validation was successful. * @returns {String} [result.error] - The validation error message if validation has failed. */ const validate = function (func, args) { if (p5.disableFriendlyErrors) { return; // skip FES } if (!Array.isArray(args)) { args = Array.from(args); } // An edge case: even when all arguments are optional and therefore, // theoretically allowed to stay undefined and valid, it is likely that the // user intended to call the function with non-undefined arguments. Skip // regular workflow and return a friendly error message right away. if ( Array.isArray(args) && args.length > 0 && args.every(arg => arg === undefined) ) { const undefinedErrorMessage = `🌸 p5.js says: All arguments for ${func}() are undefined. There is likely an error in the code.`; return { success: false, error: undefinedErrorMessage }; } let funcSchemas = schemaRegistry.get(func); if (!funcSchemas) { funcSchemas = generateZodSchemasForFunc(func); if (!funcSchemas) return; schemaRegistry.set(func, funcSchemas); } try { return { success: true, data: funcSchemas.parse(args) }; } catch (error) { const closestSchema = findClosestSchema(funcSchemas, args); const zodError = closestSchema.safeParse(args).error; const errorMessage = friendlyParamError(zodError, func, args); return { success: false, error: errorMessage }; } }; fn._validate = validate; // TEMP: For unit tests p5.decorateHelper( /^(?!_).+$/, function(target, { name }){ return function(...args){ if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) { validate(name, args); } return target.apply(this, args); }; } ); lifecycles.presetup = function(){ loadP5Constructors(); }; } if (typeof p5 !== 'undefined') { validateParams(p5, p5.prototype); } // This file was generated. Do not modify manually! var astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 80, 3, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 343, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 330, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 726, 6, 110, 6, 6, 9, 4759, 9, 787719, 239]; // This file was generated. Do not modify manually! var astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 71, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 2, 60, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 328, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 129, 74, 6, 0, 67, 12, 65, 1, 2, 0, 29, 6135, 9, 1237, 42, 9, 8936, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 496, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4153, 7, 221, 3, 5761, 15, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 4191]; // This file was generated. Do not modify manually! var nonASCIIidentifierChars = "\u200c\u200d\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0897-\u089f\u08ca-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c04\u0c3c\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0cf3\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ece\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u180f-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf-\u1ace\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\u30fb\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f\uff65"; // This file was generated. Do not modify manually! var nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u0870-\u0887\u0889-\u088e\u08a0-\u08c9\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c5d\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cdd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u1711\u171f-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4c\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c8a\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7cd\ua7d0\ua7d1\ua7d3\ua7d5-\ua7dc\ua7f2-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc"; // These are a run-length and offset encoded representation of the // >0xffff code points that are a valid part of identifiers. The // offset starts at 0x10000, and each pair of numbers represents an // offset to the next range, and then a size of the range. // Reserved word lists for various dialects of the language var reservedWords = { 3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile", 5: "class enum extends super const export import", 6: "enum", strict: "implements interface let package private protected public static yield", strictBind: "eval arguments" }; // And the keywords var ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this"; var keywords$1 = { 5: ecma5AndLessKeywords, "5module": ecma5AndLessKeywords + " export import", 6: ecma5AndLessKeywords + " const class extends export import super" }; var keywordRelationalOperator = /^in(stanceof)?$/; // ## Character categories var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]"); var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]"); // This has a complexity linear to the value of the code. The // assumption is that looking up astral identifier characters is // rare. function isInAstralSet(code, set) { var pos = 0x10000; for (var i = 0; i < set.length; i += 2) { pos += set[i]; if (pos > code) { return false } pos += set[i + 1]; if (pos >= code) { return true } } return false } // Test whether a given character code starts an identifier. function isIdentifierStart(code, astral) { if (code < 65) { return code === 36 } if (code < 91) { return true } if (code < 97) { return code === 95 } if (code < 123) { return true } if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code)) } if (astral === false) { return false } return isInAstralSet(code, astralIdentifierStartCodes) } // Test whether a given character is part of an identifier. function isIdentifierChar(code, astral) { if (code < 48) { return code === 36 } if (code < 58) { return true } if (code < 65) { return false } if (code < 91) { return true } if (code < 97) { return code === 95 } if (code < 123) { return true } if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code)) } if (astral === false) { return false } return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes) } // ## Token types // The assignment of fine-grained, information-carrying type objects // allows the tokenizer to store the information it has about a // token in a way that is very cheap for the parser to look up. // All token type variables start with an underscore, to make them // easy to recognize. // The `beforeExpr` property is used to disambiguate between regular // expressions and divisions. It is set on all token types that can // be followed by an expression (thus, a slash after them would be a // regular expression). // // The `startsExpr` property is used to check if the token ends a // `yield` expression. It is set on all token types that either can // directly start an expression (like a quotation mark) or can // continue an expression (like the body of a string). // // `isLoop` marks a keyword as starting a loop, which is important // to know when parsing a label, in order to allow or disallow // continue jumps to that label. var TokenType = function TokenType(label, conf) { if ( conf === void 0 ) conf = {}; this.label = label; this.keyword = conf.keyword; this.beforeExpr = !!conf.beforeExpr; this.startsExpr = !!conf.startsExpr; this.isLoop = !!conf.isLoop; this.isAssign = !!conf.isAssign; this.prefix = !!conf.prefix; this.postfix = !!conf.postfix; this.binop = conf.binop || null; this.updateContext = null; }; function binop(name, prec) { return new TokenType(name, {beforeExpr: true, binop: prec}) } var beforeExpr = {beforeExpr: true}, startsExpr = {startsExpr: true}; // Map keyword names to token types. var keywords = {}; // Succinct definitions of keyword token types function kw(name, options) { if ( options === void 0 ) options = {}; options.keyword = name; return keywords[name] = new TokenType(name, options) } var types$1 = { num: new TokenType("num", startsExpr), regexp: new TokenType("regexp", startsExpr), string: new TokenType("string", startsExpr), name: new TokenType("name", startsExpr), privateId: new TokenType("privateId", startsExpr), eof: new TokenType("eof"), // Punctuation token types. bracketL: new TokenType("[", {beforeExpr: true, startsExpr: true}), bracketR: new TokenType("]"), braceL: new TokenType("{", {beforeExpr: true, startsExpr: true}), braceR: new TokenType("}"), parenL: new TokenType("(", {beforeExpr: true, startsExpr: true}), parenR: new TokenType(")"), comma: new TokenType(",", beforeExpr), semi: new TokenType(";", beforeExpr), colon: new TokenType(":", beforeExpr), dot: new TokenType("."), question: new TokenType("?", beforeExpr), questionDot: new TokenType("?."), arrow: new TokenType("=>", beforeExpr), template: new TokenType("template"), invalidTemplate: new TokenType("invalidTemplate"), ellipsis: new TokenType("...", beforeExpr), backQuote: new TokenType("`", startsExpr), dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}), // Operators. These carry several kinds of properties to help the // parser use them properly (the presence of these properties is // what categorizes them as operators). // // `binop`, when present, specifies that this operator is a binary // operator, and will refer to its precedence. // // `prefix` and `postfix` mark the operator as a prefix or postfix // unary operator. // // `isAssign` marks all of `=`, `+=`, `-=` etcetera, which act as // binary operators with a very low precedence, that should result // in AssignmentExpression nodes. eq: new TokenType("=", {beforeExpr: true, isAssign: true}), assign: new TokenType("_=", {beforeExpr: true, isAssign: true}), incDec: new TokenType("++/--", {prefix: true, postfix: true, startsExpr: true}), prefix: new TokenType("!/~", {beforeExpr: true, prefix: true, startsExpr: true}), logicalOR: binop("||", 1), logicalAND: binop("&&", 2), bitwiseOR: binop("|", 3), bitwiseXOR: binop("^", 4), bitwiseAND: binop("&", 5), equality: binop("==/!=/===/!==", 6), relational: binop("/<=/>=", 7), bitShift: binop("<>/>>>", 8), plusMin: new TokenType("+/-", {beforeExpr: true, binop: 9, prefix: true, startsExpr: true}), modulo: binop("%", 10), star: binop("*", 10), slash: binop("/", 10), starstar: new TokenType("**", {beforeExpr: true}), coalesce: binop("??", 1), // Keyword token types. _break: kw("break"), _case: kw("case", beforeExpr), _catch: kw("catch"), _continue: kw("continue"), _debugger: kw("debugger"), _default: kw("default", beforeExpr), _do: kw("do", {isLoop: true, beforeExpr: true}), _else: kw("else", beforeExpr), _finally: kw("finally"), _for: kw("for", {isLoop: true}), _function: kw("function", startsExpr), _if: kw("if"), _return: kw("return", beforeExpr), _switch: kw("switch"), _throw: kw("throw", beforeExpr), _try: kw("try"), _var: kw("var"), _const: kw("const"), _while: kw("while", {isLoop: true}), _with: kw("with"), _new: kw("new", {beforeExpr: true, startsExpr: true}), _this: kw("this", startsExpr), _super: kw("super", startsExpr), _class: kw("class", startsExpr), _extends: kw("extends", beforeExpr), _export: kw("export"), _import: kw("import", startsExpr), _null: kw("null", startsExpr), _true: kw("true", startsExpr), _false: kw("false", startsExpr), _in: kw("in", {beforeExpr: true, binop: 7}), _instanceof: kw("instanceof", {beforeExpr: true, binop: 7}), _typeof: kw("typeof", {beforeExpr: true, prefix: true, startsExpr: true}), _void: kw("void", {beforeExpr: true, prefix: true, startsExpr: true}), _delete: kw("delete", {beforeExpr: true, prefix: true, startsExpr: true}) }; // Matches a whole line break (where CRLF is considered a single // line break). Used to count lines. var lineBreak = /\r\n?|\n|\u2028|\u2029/; var lineBreakG = new RegExp(lineBreak.source, "g"); function isNewLine(code) { return code === 10 || code === 13 || code === 0x2028 || code === 0x2029 } function nextLineBreak(code, from, end) { if ( end === void 0 ) end = code.length; for (var i = from; i < end; i++) { var next = code.charCodeAt(i); if (isNewLine(next)) { return i < end - 1 && next === 13 && code.charCodeAt(i + 1) === 10 ? i + 2 : i + 1 } } return -1 } var nonASCIIwhitespace = /[\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]/; var skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g; var ref = Object.prototype; var hasOwnProperty = ref.hasOwnProperty; var toString$1 = ref.toString; var hasOwn = Object.hasOwn || (function (obj, propName) { return ( hasOwnProperty.call(obj, propName) ); }); var isArray = Array.isArray || (function (obj) { return ( toString$1.call(obj) === "[object Array]" ); }); var regexpCache = Object.create(null); function wordsRegexp(words) { return regexpCache[words] || (regexpCache[words] = new RegExp("^(?:" + words.replace(/ /g, "|") + ")$")) } function codePointToString(code) { // UTF-16 Decoding if (code <= 0xFFFF) { return String.fromCharCode(code) } code -= 0x10000; return String.fromCharCode((code >> 10) + 0xD800, (code & 1023) + 0xDC00) } var loneSurrogate = /(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/; // These are used when `options.locations` is on, for the // `startLoc` and `endLoc` properties. var Position = function Position(line, col) { this.line = line; this.column = col; }; Position.prototype.offset = function offset (n) { return new Position(this.line, this.column + n) }; var SourceLocation = function SourceLocation(p, start, end) { this.start = start; this.end = end; if (p.sourceFile !== null) { this.source = p.sourceFile; } }; // The `getLineInfo` function is mostly useful when the // `locations` option is off (for performance reasons) and you // want to find the line/column position for a given character // offset. `input` should be the code string that the offset refers // into. function getLineInfo(input, offset) { for (var line = 1, cur = 0;;) { var nextBreak = nextLineBreak(input, cur, offset); if (nextBreak < 0) { return new Position(line, offset - cur) } ++line; cur = nextBreak; } } // A second argument must be given to configure the parser process. // These options are recognized (only `ecmaVersion` is required): var defaultOptions = { // `ecmaVersion` indicates the ECMAScript version to parse. Must be // either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10 // (2019), 11 (2020), 12 (2021), 13 (2022), 14 (2023), or `"latest"` // (the latest version the library supports). This influences // support for strict mode, the set of reserved words, and support // for new syntax features. ecmaVersion: null, // `sourceType` indicates the mode the code should be parsed in. // Can be either `"script"` or `"module"`. This influences global // strict mode and parsing of `import` and `export` declarations. sourceType: "script", // `onInsertedSemicolon` can be a callback that will be called when // a semicolon is automatically inserted. It will be passed the // position of the inserted semicolon as an offset, and if // `locations` is enabled, it is given the location as a `{line, // column}` object as second argument. onInsertedSemicolon: null, // `onTrailingComma` is similar to `onInsertedSemicolon`, but for // trailing commas. onTrailingComma: null, // By default, reserved words are only enforced if ecmaVersion >= 5. // Set `allowReserved` to a boolean value to explicitly turn this on // an off. When this option has the value "never", reserved words // and keywords can also not be used as property names. allowReserved: null, // When enabled, a return at the top level is not considered an // error. allowReturnOutsideFunction: false, // When enabled, import/export statements are not constrained to // appearing at the top of the program, and an import.meta expression // in a script isn't considered an error. allowImportExportEverywhere: false, // By default, await identifiers are allowed to appear at the top-level scope only if ecmaVersion >= 2022. // When enabled, await identifiers are allowed to appear at the top-level scope, // but they are still not allowed in non-async functions. allowAwaitOutsideFunction: null, // When enabled, super identifiers are not constrained to // appearing in methods and do not raise an error when they appear elsewhere. allowSuperOutsideMethod: null, // When enabled, hashbang directive in the beginning of file is // allowed and treated as a line comment. Enabled by default when // `ecmaVersion` >= 2023. allowHashBang: false, // By default, the parser will verify that private properties are // only used in places where they are valid and have been declared. // Set this to false to turn such checks off. checkPrivateFields: true, // When `locations` is on, `loc` properties holding objects with // `start` and `end` properties in `{line, column}` form (with // line being 1-based and column 0-based) will be attached to the // nodes. locations: false, // A function can be passed as `onToken` option, which will // cause Acorn to call that function with object in the same // format as tokens returned from `tokenizer().getToken()`. Note // that you are not allowed to call the parser from the // callback—that will corrupt its internal state. onToken: null, // A function can be passed as `onComment` option, which will // cause Acorn to call that function with `(block, text, start, // end)` parameters whenever a comment is skipped. `block` is a // boolean indicating whether this is a block (`/* */`) comment, // `text` is the content of the comment, and `start` and `end` are // character offsets that denote the start and end of the comment. // When the `locations` option is on, two more parameters are // passed, the full `{line, column}` locations of the start and // end of the comments. Note that you are not allowed to call the // parser from the callback—that will corrupt its internal state. // When this option has an array as value, objects representing the // comments are pushed to it. onComment: null, // Nodes have their start and end characters offsets recorded in // `start` and `end` properties (directly on the node, rather than // the `loc` object, which holds line/column data. To also add a // [semi-standardized][range] `range` property holding a `[start, // end]` array with the same numbers, set the `ranges` option to // `true`. // // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678 ranges: false, // It is possible to parse multiple files into a single AST by // passing the tree produced by parsing the first file as // `program` option in subsequent parses. This will add the // toplevel forms of the parsed file to the `Program` (top) node // of an existing parse tree. program: null, // When `locations` is on, you can pass this to record the source // file in every node's `loc` object. sourceFile: null, // This value, if given, is stored in every node, whether // `locations` is on or off. directSourceFile: null, // When enabled, parenthesized expressions are represented by // (non-standard) ParenthesizedExpression nodes preserveParens: false }; // Interpret and default an options object var warnedAboutEcmaVersion = false; function getOptions(opts) { var options = {}; for (var opt in defaultOptions) { options[opt] = opts && hasOwn(opts, opt) ? opts[opt] : defaultOptions[opt]; } if (options.ecmaVersion === "latest") { options.ecmaVersion = 1e8; } else if (options.ecmaVersion == null) { if (!warnedAboutEcmaVersion && typeof console === "object" && console.warn) { warnedAboutEcmaVersion = true; console.warn("Since Acorn 8.0.0, options.ecmaVersion is required.\nDefaulting to 2020, but this will stop working in the future."); } options.ecmaVersion = 11; } else if (options.ecmaVersion >= 2015) { options.ecmaVersion -= 2009; } if (options.allowReserved == null) { options.allowReserved = options.ecmaVersion < 5; } if (!opts || opts.allowHashBang == null) { options.allowHashBang = options.ecmaVersion >= 14; } if (isArray(options.onToken)) { var tokens = options.onToken; options.onToken = function (token) { return tokens.push(token); }; } if (isArray(options.onComment)) { options.onComment = pushComment(options, options.onComment); } return options } function pushComment(options, array) { return function(block, text, start, end, startLoc, endLoc) { var comment = { type: block ? "Block" : "Line", value: text, start: start, end: end }; if (options.locations) { comment.loc = new SourceLocation(this, startLoc, endLoc); } if (options.ranges) { comment.range = [start, end]; } array.push(comment); } } // Each scope gets a bitset that may contain these flags var SCOPE_TOP = 1, SCOPE_FUNCTION = 2, SCOPE_ASYNC = 4, SCOPE_GENERATOR = 8, SCOPE_ARROW = 16, SCOPE_SIMPLE_CATCH = 32, SCOPE_SUPER = 64, SCOPE_DIRECT_SUPER = 128, SCOPE_CLASS_STATIC_BLOCK = 256, SCOPE_CLASS_FIELD_INIT = 512, SCOPE_VAR = SCOPE_TOP | SCOPE_FUNCTION | SCOPE_CLASS_STATIC_BLOCK; function functionFlags(async, generator) { return SCOPE_FUNCTION | (async ? SCOPE_ASYNC : 0) | (generator ? SCOPE_GENERATOR : 0) } // Used in checkLVal* and declareName to determine the type of a binding var BIND_NONE = 0, // Not a binding BIND_VAR = 1, // Var-style binding BIND_LEXICAL = 2, // Let- or const-style binding BIND_FUNCTION = 3, // Function declaration BIND_SIMPLE_CATCH = 4, // Simple (identifier pattern) catch binding BIND_OUTSIDE = 5; // Special case for function names as bound inside the function var Parser = function Parser(options, input, startPos) { this.options = options = getOptions(options); this.sourceFile = options.sourceFile; this.keywords = wordsRegexp(keywords$1[options.ecmaVersion >= 6 ? 6 : options.sourceType === "module" ? "5module" : 5]); var reserved = ""; if (options.allowReserved !== true) { reserved = reservedWords[options.ecmaVersion >= 6 ? 6 : options.ecmaVersion === 5 ? 5 : 3]; if (options.sourceType === "module") { reserved += " await"; } } this.reservedWords = wordsRegexp(reserved); var reservedStrict = (reserved ? reserved + " " : "") + reservedWords.strict; this.reservedWordsStrict = wordsRegexp(reservedStrict); this.reservedWordsStrictBind = wordsRegexp(reservedStrict + " " + reservedWords.strictBind); this.input = String(input); // Used to signal to callers of `readWord1` whether the word // contained any escape sequences. This is needed because words with // escape sequences must not be interpreted as keywords. this.containsEsc = false; // Set up token state // The current position of the tokenizer in the input. if (startPos) { this.pos = startPos; this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1; this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length; } else { this.pos = this.lineStart = 0; this.curLine = 1; } // Properties of the current token: // Its type this.type = types$1.eof; // For tokens that include more information than their type, the value this.value = null; // Its start and end offset this.start = this.end = this.pos; // And, if locations are used, the {line, column} object // corresponding to those offsets this.startLoc = this.endLoc = this.curPosition(); // Position information for the previous token this.lastTokEndLoc = this.lastTokStartLoc = null; this.lastTokStart = this.lastTokEnd = this.pos; // The context stack is used to superficially track syntactic // context to predict whether a regular expression is allowed in a // given position. this.context = this.initialContext(); this.exprAllowed = true; // Figure out if it's a module code. this.inModule = options.sourceType === "module"; this.strict = this.inModule || this.strictDirective(this.pos); // Used to signify the start of a potential arrow function this.potentialArrowAt = -1; this.potentialArrowInForAwait = false; // Positions to delayed-check that yield/await does not exist in default parameters. this.yieldPos = this.awaitPos = this.awaitIdentPos = 0; // Labels in scope. this.labels = []; // Thus-far undefined exports. this.undefinedExports = Object.create(null); // If enabled, skip leading hashbang line. if (this.pos === 0 && options.allowHashBang && this.input.slice(0, 2) === "#!") { this.skipLineComment(2); } // Scope tracking for duplicate variable names (see scope.js) this.scopeStack = []; this.enterScope(SCOPE_TOP); // For RegExp validation this.regexpState = null; // The stack of private names. // Each element has two properties: 'declared' and 'used'. // When it exited from the outermost class definition, all used private names must be declared. this.privateNameStack = []; }; var prototypeAccessors = { inFunction: { configurable: true },inGenerator: { configurable: true },inAsync: { configurable: true },canAwait: { configurable: true },allowSuper: { configurable: true },allowDirectSuper: { configurable: true },treatFunctionsAsVar: { configurable: true },allowNewDotTarget: { configurable: true },inClassStaticBlock: { configurable: true } }; Parser.prototype.parse = function parse () { var node = this.options.program || this.startNode(); this.nextToken(); return this.parseTopLevel(node) }; prototypeAccessors.inFunction.get = function () { return (this.currentVarScope().flags & SCOPE_FUNCTION) > 0 }; prototypeAccessors.inGenerator.get = function () { return (this.currentVarScope().flags & SCOPE_GENERATOR) > 0 }; prototypeAccessors.inAsync.get = function () { return (this.currentVarScope().flags & SCOPE_ASYNC) > 0 }; prototypeAccessors.canAwait.get = function () { for (var i = this.scopeStack.length - 1; i >= 0; i--) { var ref = this.scopeStack[i]; var flags = ref.flags; if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT)) { return false } if (flags & SCOPE_FUNCTION) { return (flags & SCOPE_ASYNC) > 0 } } return (this.inModule && this.options.ecmaVersion >= 13) || this.options.allowAwaitOutsideFunction }; prototypeAccessors.allowSuper.get = function () { var ref = this.currentThisScope(); var flags = ref.flags; return (flags & SCOPE_SUPER) > 0 || this.options.allowSuperOutsideMethod }; prototypeAccessors.allowDirectSuper.get = function () { return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0 }; prototypeAccessors.treatFunctionsAsVar.get = function () { return this.treatFunctionsAsVarInScope(this.currentScope()) }; prototypeAccessors.allowNewDotTarget.get = function () { for (var i = this.scopeStack.length - 1; i >= 0; i--) { var ref = this.scopeStack[i]; var flags = ref.flags; if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT) || ((flags & SCOPE_FUNCTION) && !(flags & SCOPE_ARROW))) { return true } } return false }; prototypeAccessors.inClassStaticBlock.get = function () { return (this.currentVarScope().flags & SCOPE_CLASS_STATIC_BLOCK) > 0 }; Parser.extend = function extend () { var plugins = [], len = arguments.length; while ( len-- ) plugins[ len ] = arguments[ len ]; var cls = this; for (var i = 0; i < plugins.length; i++) { cls = plugins[i](cls); } return cls }; Parser.parse = function parse (input, options) { return new this(options, input).parse() }; Parser.parseExpressionAt = function parseExpressionAt (input, pos, options) { var parser = new this(options, input, pos); parser.nextToken(); return parser.parseExpression() }; Parser.tokenizer = function tokenizer (input, options) { return new this(options, input) }; Object.defineProperties( Parser.prototype, prototypeAccessors ); var pp$9 = Parser.prototype; // ## Parser utilities var literal = /^(?:'((?:\\[^]|[^'\\])*?)'|"((?:\\[^]|[^"\\])*?)")/; pp$9.strictDirective = function(start) { if (this.options.ecmaVersion < 5) { return false } for (;;) { // Try to find string literal. skipWhiteSpace.lastIndex = start; start += skipWhiteSpace.exec(this.input)[0].length; var match = literal.exec(this.input.slice(start)); if (!match) { return false } if ((match[1] || match[2]) === "use strict") { skipWhiteSpace.lastIndex = start + match[0].length; var spaceAfter = skipWhiteSpace.exec(this.input), end = spaceAfter.index + spaceAfter[0].length; var next = this.input.charAt(end); return next === ";" || next === "}" || (lineBreak.test(spaceAfter[0]) && !(/[(`.[+\-/*%<>=,?^&]/.test(next) || next === "!" && this.input.charAt(end + 1) === "=")) } start += match[0].length; // Skip semicolon, if any. skipWhiteSpace.lastIndex = start; start += skipWhiteSpace.exec(this.input)[0].length; if (this.input[start] === ";") { start++; } } }; // Predicate that tests whether the next token is of the given // type, and if yes, consumes it as a side effect. pp$9.eat = function(type) { if (this.type === type) { this.next(); return true } else { return false } }; // Tests whether parsed token is a contextual keyword. pp$9.isContextual = function(name) { return this.type === types$1.name && this.value === name && !this.containsEsc }; // Consumes contextual keyword if possible. pp$9.eatContextual = function(name) { if (!this.isContextual(name)) { return false } this.next(); return true }; // Asserts that following token is given contextual keyword. pp$9.expectContextual = function(name) { if (!this.eatContextual(name)) { this.unexpected(); } }; // Test whether a semicolon can be inserted at the current position. pp$9.canInsertSemicolon = function() { return this.type === types$1.eof || this.type === types$1.braceR || lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) }; pp$9.insertSemicolon = function() { if (this.canInsertSemicolon()) { if (this.options.onInsertedSemicolon) { this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc); } return true } }; // Consume a semicolon, or, failing that, see if we are allowed to // pretend that there is a semicolon at this position. pp$9.semicolon = function() { if (!this.eat(types$1.semi) && !this.insertSemicolon()) { this.unexpected(); } }; pp$9.afterTrailingComma = function(tokType, notNext) { if (this.type === tokType) { if (this.options.onTrailingComma) { this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc); } if (!notNext) { this.next(); } return true } }; // Expect a token of a given type. If found, consume it, otherwise, // raise an unexpected token error. pp$9.expect = function(type) { this.eat(type) || this.unexpected(); }; // Raise an unexpected token error. pp$9.unexpected = function(pos) { this.raise(pos != null ? pos : this.start, "Unexpected token"); }; var DestructuringErrors = function DestructuringErrors() { this.shorthandAssign = this.trailingComma = this.parenthesizedAssign = this.parenthesizedBind = this.doubleProto = -1; }; pp$9.checkPatternErrors = function(refDestructuringErrors, isAssign) { if (!refDestructuringErrors) { return } if (refDestructuringErrors.trailingComma > -1) { this.raiseRecoverable(refDestructuringErrors.trailingComma, "Comma is not permitted after the rest element"); } var parens = isAssign ? refDestructuringErrors.parenthesizedAssign : refDestructuringErrors.parenthesizedBind; if (parens > -1) { this.raiseRecoverable(parens, isAssign ? "Assigning to rvalue" : "Parenthesized pattern"); } }; pp$9.checkExpressionErrors = function(refDestructuringErrors, andThrow) { if (!refDestructuringErrors) { return false } var shorthandAssign = refDestructuringErrors.shorthandAssign; var doubleProto = refDestructuringErrors.doubleProto; if (!andThrow) { return shorthandAssign >= 0 || doubleProto >= 0 } if (shorthandAssign >= 0) { this.raise(shorthandAssign, "Shorthand property assignments are valid only in destructuring patterns"); } if (doubleProto >= 0) { this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property"); } }; pp$9.checkYieldAwaitInDefaultParams = function() { if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos)) { this.raise(this.yieldPos, "Yield expression cannot be a default value"); } if (this.awaitPos) { this.raise(this.awaitPos, "Await expression cannot be a default value"); } }; pp$9.isSimpleAssignTarget = function(expr) { if (expr.type === "ParenthesizedExpression") { return this.isSimpleAssignTarget(expr.expression) } return expr.type === "Identifier" || expr.type === "MemberExpression" }; var pp$8 = Parser.prototype; // ### Statement parsing // Parse a program. Initializes the parser, reads any number of // statements, and wraps them in a Program node. Optionally takes a // `program` argument. If present, the statements will be appended // to its body instead of creating a new node. pp$8.parseTopLevel = function(node) { var exports$1 = Object.create(null); if (!node.body) { node.body = []; } while (this.type !== types$1.eof) { var stmt = this.parseStatement(null, true, exports$1); node.body.push(stmt); } if (this.inModule) { for (var i = 0, list = Object.keys(this.undefinedExports); i < list.length; i += 1) { var name = list[i]; this.raiseRecoverable(this.undefinedExports[name].start, ("Export '" + name + "' is not defined")); } } this.adaptDirectivePrologue(node.body); this.next(); node.sourceType = this.options.sourceType; return this.finishNode(node, "Program") }; var loopLabel = {kind: "loop"}, switchLabel = {kind: "switch"}; pp$8.isLet = function(context) { if (this.options.ecmaVersion < 6 || !this.isContextual("let")) { return false } skipWhiteSpace.lastIndex = this.pos; var skip = skipWhiteSpace.exec(this.input); var next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next); // For ambiguous cases, determine if a LexicalDeclaration (or only a // Statement) is allowed here. If context is not empty then only a Statement // is allowed. However, `let [` is an explicit negative lookahead for // ExpressionStatement, so special-case it first. if (nextCh === 91 || nextCh === 92) { return true } // '[', '\' if (context) { return false } if (nextCh === 123 || nextCh > 0xd7ff && nextCh < 0xdc00) { return true } // '{', astral if (isIdentifierStart(nextCh, true)) { var pos = next + 1; while (isIdentifierChar(nextCh = this.input.charCodeAt(pos), true)) { ++pos; } if (nextCh === 92 || nextCh > 0xd7ff && nextCh < 0xdc00) { return true } var ident = this.input.slice(next, pos); if (!keywordRelationalOperator.test(ident)) { return true } } return false }; // check 'async [no LineTerminator here] function' // - 'async /*foo*/ function' is OK. // - 'async /*\n*/ function' is invalid. pp$8.isAsyncFunction = function() { if (this.options.ecmaVersion < 8 || !this.isContextual("async")) { return false } skipWhiteSpace.lastIndex = this.pos; var skip = skipWhiteSpace.exec(this.input); var next = this.pos + skip[0].length, after; return !lineBreak.test(this.input.slice(this.pos, next)) && this.input.slice(next, next + 8) === "function" && (next + 8 === this.input.length || !(isIdentifierChar(after = this.input.charCodeAt(next + 8)) || after > 0xd7ff && after < 0xdc00)) }; pp$8.isUsingKeyword = function(isAwaitUsing, isFor) { if (this.options.ecmaVersion < 17 || !this.isContextual(isAwaitUsing ? "await" : "using")) { return false } skipWhiteSpace.lastIndex = this.pos; var skip = skipWhiteSpace.exec(this.input); var next = this.pos + skip[0].length; if (lineBreak.test(this.input.slice(this.pos, next))) { return false } if (isAwaitUsing) { var awaitEndPos = next + 5 /* await */, after; if (this.input.slice(next, awaitEndPos) !== "using" || awaitEndPos === this.input.length || isIdentifierChar(after = this.input.charCodeAt(awaitEndPos)) || (after > 0xd7ff && after < 0xdc00) ) { return false } skipWhiteSpace.lastIndex = awaitEndPos; var skipAfterUsing = skipWhiteSpace.exec(this.input); if (skipAfterUsing && lineBreak.test(this.input.slice(awaitEndPos, awaitEndPos + skipAfterUsing[0].length))) { return false } } if (isFor) { var ofEndPos = next + 2 /* of */, after$1; if (this.input.slice(next, ofEndPos) === "of") { if (ofEndPos === this.input.length || (!isIdentifierChar(after$1 = this.input.charCodeAt(ofEndPos)) && !(after$1 > 0xd7ff && after$1 < 0xdc00))) { return false } } } var ch = this.input.charCodeAt(next); return isIdentifierStart(ch, true) || ch === 92 // '\' }; pp$8.isAwaitUsing = function(isFor) { return this.isUsingKeyword(true, isFor) }; pp$8.isUsing = function(isFor) { return this.isUsingKeyword(false, isFor) }; // Parse a single statement. // // If expecting a statement and finding a slash operator, parse a // regular expression literal. This is to handle cases like // `if (foo) /blah/.exec(foo)`, where looking at the previous token // does not help. pp$8.parseStatement = function(context, topLevel, exports$1) { var starttype = this.type, node = this.startNode(), kind; if (this.isLet(context)) { starttype = types$1._var; kind = "let"; } // Most types of statements are recognized by the keyword they // start with. Many are trivial to parse, some require a bit of // complexity. switch (starttype) { case types$1._break: case types$1._continue: return this.parseBreakContinueStatement(node, starttype.keyword) case types$1._debugger: return this.parseDebuggerStatement(node) case types$1._do: return this.parseDoStatement(node) case types$1._for: return this.parseForStatement(node) case types$1._function: // Function as sole body of either an if statement or a labeled statement // works, but not when it is part of a labeled statement that is the sole // body of an if statement. if ((context && (this.strict || context !== "if" && context !== "label")) && this.options.ecmaVersion >= 6) { this.unexpected(); } return this.parseFunctionStatement(node, false, !context) case types$1._class: if (context) { this.unexpected(); } return this.parseClass(node, true) case types$1._if: return this.parseIfStatement(node) case types$1._return: return this.parseReturnStatement(node) case types$1._switch: return this.parseSwitchStatement(node) case types$1._throw: return this.parseThrowStatement(node) case types$1._try: return this.parseTryStatement(node) case types$1._const: case types$1._var: kind = kind || this.value; if (context && kind !== "var") { this.unexpected(); } return this.parseVarStatement(node, kind) case types$1._while: return this.parseWhileStatement(node) case types$1._with: return this.parseWithStatement(node) case types$1.braceL: return this.parseBlock(true, node) case types$1.semi: return this.parseEmptyStatement(node) case types$1._export: case types$1._import: if (this.options.ecmaVersion > 10 && starttype === types$1._import) { skipWhiteSpace.lastIndex = this.pos; var skip = skipWhiteSpace.exec(this.input); var next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next); if (nextCh === 40 || nextCh === 46) // '(' or '.' { return this.parseExpressionStatement(node, this.parseExpression()) } } if (!this.options.allowImportExportEverywhere) { if (!topLevel) { this.raise(this.start, "'import' and 'export' may only appear at the top level"); } if (!this.inModule) { this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'"); } } return starttype === types$1._import ? this.parseImport(node) : this.parseExport(node, exports$1) // If the statement does not start with a statement keyword or a // brace, it's an ExpressionStatement or LabeledStatement. We // simply start parsing an expression, and afterwards, if the // next token is a colon and the expression was a simple // Identifier node, we switch to interpreting it as a label. default: if (this.isAsyncFunction()) { if (context) { this.unexpected(); } this.next(); return this.parseFunctionStatement(node, true, !context) } var usingKind = this.isAwaitUsing(false) ? "await using" : this.isUsing(false) ? "using" : null; if (usingKind) { if (topLevel && this.options.sourceType === "script") { this.raise(this.start, "Using declaration cannot appear in the top level when source type is `script`"); } if (usingKind === "await using") { if (!this.canAwait) { this.raise(this.start, "Await using cannot appear outside of async function"); } this.next(); } this.next(); this.parseVar(node, false, usingKind); this.semicolon(); return this.finishNode(node, "VariableDeclaration") } var maybeName = this.value, expr = this.parseExpression(); if (starttype === types$1.name && expr.type === "Identifier" && this.eat(types$1.colon)) { return this.parseLabeledStatement(node, maybeName, expr, context) } else { return this.parseExpressionStatement(node, expr) } } }; pp$8.parseBreakContinueStatement = function(node, keyword) { var isBreak = keyword === "break"; this.next(); if (this.eat(types$1.semi) || this.insertSemicolon()) { node.label = null; } else if (this.type !== types$1.name) { this.unexpected(); } else { node.label = this.parseIdent(); this.semicolon(); } // Verify that there is an actual destination to break or // continue to. var i = 0; for (; i < this.labels.length; ++i) { var lab = this.labels[i]; if (node.label == null || lab.name === node.label.name) { if (lab.kind != null && (isBreak || lab.kind === "loop")) { break } if (node.label && isBreak) { break } } } if (i === this.labels.length) { this.raise(node.start, "Unsyntactic " + keyword); } return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement") }; pp$8.parseDebuggerStatement = function(node) { this.next(); this.semicolon(); return this.finishNode(node, "DebuggerStatement") }; pp$8.parseDoStatement = function(node) { this.next(); this.labels.push(loopLabel); node.body = this.parseStatement("do"); this.labels.pop(); this.expect(types$1._while); node.test = this.parseParenExpression(); if (this.options.ecmaVersion >= 6) { this.eat(types$1.semi); } else { this.semicolon(); } return this.finishNode(node, "DoWhileStatement") }; // Disambiguating between a `for` and a `for`/`in` or `for`/`of` // loop is non-trivial. Basically, we have to parse the init `var` // statement or expression, disallowing the `in` operator (see // the second parameter to `parseExpression`), and then check // whether the next token is `in` or `of`. When there is no init // part (semicolon immediately after the opening parenthesis), it // is a regular `for` loop. pp$8.parseForStatement = function(node) { this.next(); var awaitAt = (this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual("await")) ? this.lastTokStart : -1; this.labels.push(loopLabel); this.enterScope(0); this.expect(types$1.parenL); if (this.type === types$1.semi) { if (awaitAt > -1) { this.unexpected(awaitAt); } return this.parseFor(node, null) } var isLet = this.isLet(); if (this.type === types$1._var || this.type === types$1._const || isLet) { var init$1 = this.startNode(), kind = isLet ? "let" : this.value; this.next(); this.parseVar(init$1, true, kind); this.finishNode(init$1, "VariableDeclaration"); return this.parseForAfterInit(node, init$1, awaitAt) } var startsWithLet = this.isContextual("let"), isForOf = false; var usingKind = this.isUsing(true) ? "using" : this.isAwaitUsing(true) ? "await using" : null; if (usingKind) { var init$2 = this.startNode(); this.next(); if (usingKind === "await using") { this.next(); } this.parseVar(init$2, true, usingKind); this.finishNode(init$2, "VariableDeclaration"); return this.parseForAfterInit(node, init$2, awaitAt) } var containsEsc = this.containsEsc; var refDestructuringErrors = new DestructuringErrors; var initPos = this.start; var init = awaitAt > -1 ? this.parseExprSubscripts(refDestructuringErrors, "await") : this.parseExpression(true, refDestructuringErrors); if (this.type === types$1._in || (isForOf = this.options.ecmaVersion >= 6 && this.isContextual("of"))) { if (awaitAt > -1) { // implies `ecmaVersion >= 9` (see declaration of awaitAt) if (this.type === types$1._in) { this.unexpected(awaitAt); } node.await = true; } else if (isForOf && this.options.ecmaVersion >= 8) { if (init.start === initPos && !containsEsc && init.type === "Identifier" && init.name === "async") { this.unexpected(); } else if (this.options.ecmaVersion >= 9) { node.await = false; } } if (startsWithLet && isForOf) { this.raise(init.start, "The left-hand side of a for-of loop may not start with 'let'."); } this.toAssignable(init, false, refDestructuringErrors); this.checkLValPattern(init); return this.parseForIn(node, init) } else { this.checkExpressionErrors(refDestructuringErrors, true); } if (awaitAt > -1) { this.unexpected(awaitAt); } return this.parseFor(node, init) }; // Helper method to parse for loop after variable initialization pp$8.parseForAfterInit = function(node, init, awaitAt) { if ((this.type === types$1._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1) { if (this.options.ecmaVersion >= 9) { if (this.type === types$1._in) { if (awaitAt > -1) { this.unexpected(awaitAt); } } else { node.await = awaitAt > -1; } } return this.parseForIn(node, init) } if (awaitAt > -1) { this.unexpected(awaitAt); } return this.parseFor(node, init) }; pp$8.parseFunctionStatement = function(node, isAsync, declarationPosition) { this.next(); return this.parseFunction(node, FUNC_STATEMENT | (declarationPosition ? 0 : FUNC_HANGING_STATEMENT), false, isAsync) }; pp$8.parseIfStatement = function(node) { this.next(); node.test = this.parseParenExpression(); // allow function declarations in branches, but only in non-strict mode node.consequent = this.parseStatement("if"); node.alternate = this.eat(types$1._else) ? this.parseStatement("if") : null; return this.finishNode(node, "IfStatement") }; pp$8.parseReturnStatement = function(node) { if (!this.inFunction && !this.options.allowReturnOutsideFunction) { this.raise(this.start, "'return' outside of function"); } this.next(); // In `return` (and `break`/`continue`), the keywords with // optional arguments, we eagerly look for a semicolon or the // possibility to insert one. if (this.eat(types$1.semi) || this.insertSemicolon()) { node.argument = null; } else { node.argument = this.parseExpression(); this.semicolon(); } return this.finishNode(node, "ReturnStatement") }; pp$8.parseSwitchStatement = function(node) { this.next(); node.discriminant = this.parseParenExpression(); node.cases = []; this.expect(types$1.braceL); this.labels.push(switchLabel); this.enterScope(0); // Statements under must be grouped (by label) in SwitchCase // nodes. `cur` is used to keep the node that we are currently // adding statements to. var cur; for (var sawDefault = false; this.type !== types$1.braceR;) { if (this.type === types$1._case || this.type === types$1._default) { var isCase = this.type === types$1._case; if (cur) { this.finishNode(cur, "SwitchCase"); } node.cases.push(cur = this.startNode()); cur.consequent = []; this.next(); if (isCase) { cur.test = this.parseExpression(); } else { if (sawDefault) { this.raiseRecoverable(this.lastTokStart, "Multiple default clauses"); } sawDefault = true; cur.test = null; } this.expect(types$1.colon); } else { if (!cur) { this.unexpected(); } cur.consequent.push(this.parseStatement(null)); } } this.exitScope(); if (cur) { this.finishNode(cur, "SwitchCase"); } this.next(); // Closing brace this.labels.pop(); return this.finishNode(node, "SwitchStatement") }; pp$8.parseThrowStatement = function(node) { this.next(); if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) { this.raise(this.lastTokEnd, "Illegal newline after throw"); } node.argument = this.parseExpression(); this.semicolon(); return this.finishNode(node, "ThrowStatement") }; // Reused empty array added for node fields that are always empty. var empty$1 = []; pp$8.parseCatchClauseParam = function() { var param = this.parseBindingAtom(); var simple = param.type === "Identifier"; this.enterScope(simple ? SCOPE_SIMPLE_CATCH : 0); this.checkLValPattern(param, simple ? BIND_SIMPLE_CATCH : BIND_LEXICAL); this.expect(types$1.parenR); return param }; pp$8.parseTryStatement = function(node) { this.next(); node.block = this.parseBlock(); node.handler = null; if (this.type === types$1._catch) { var clause = this.startNode(); this.next(); if (this.eat(types$1.parenL)) { clause.param = this.parseCatchClauseParam(); } else { if (this.options.ecmaVersion < 10) { this.unexpected(); } clause.param = null; this.enterScope(0); } clause.body = this.parseBlock(false); this.exitScope(); node.handler = this.finishNode(clause, "CatchClause"); } node.finalizer = this.eat(types$1._finally) ? this.parseBlock() : null; if (!node.handler && !node.finalizer) { this.raise(node.start, "Missing catch or finally clause"); } return this.finishNode(node, "TryStatement") }; pp$8.parseVarStatement = function(node, kind, allowMissingInitializer) { this.next(); this.parseVar(node, false, kind, allowMissingInitializer); this.semicolon(); return this.finishNode(node, "VariableDeclaration") }; pp$8.parseWhileStatement = function(node) { this.next(); node.test = this.parseParenExpression(); this.labels.push(loopLabel); node.body = this.parseStatement("while"); this.labels.pop(); return this.finishNode(node, "WhileStatement") }; pp$8.parseWithStatement = function(node) { if (this.strict) { this.raise(this.start, "'with' in strict mode"); } this.next(); node.object = this.parseParenExpression(); node.body = this.parseStatement("with"); return this.finishNode(node, "WithStatement") }; pp$8.parseEmptyStatement = function(node) { this.next(); return this.finishNode(node, "EmptyStatement") }; pp$8.parseLabeledStatement = function(node, maybeName, expr, context) { for (var i$1 = 0, list = this.labels; i$1 < list.length; i$1 += 1) { var label = list[i$1]; if (label.name === maybeName) { this.raise(expr.start, "Label '" + maybeName + "' is already declared"); } } var kind = this.type.isLoop ? "loop" : this.type === types$1._switch ? "switch" : null; for (var i = this.labels.length - 1; i >= 0; i--) { var label$1 = this.labels[i]; if (label$1.statementStart === node.start) { // Update information about previous labels on this node label$1.statementStart = this.start; label$1.kind = kind; } else { break } } this.labels.push({name: maybeName, kind: kind, statementStart: this.start}); node.body = this.parseStatement(context ? context.indexOf("label") === -1 ? context + "label" : context : "label"); this.labels.pop(); node.label = expr; return this.finishNode(node, "LabeledStatement") }; pp$8.parseExpressionStatement = function(node, expr) { node.expression = expr; this.semicolon(); return this.finishNode(node, "ExpressionStatement") }; // Parse a semicolon-enclosed block of statements, handling `"use // strict"` declarations when `allowStrict` is true (used for // function bodies). pp$8.parseBlock = function(createNewLexicalScope, node, exitStrict) { if ( createNewLexicalScope === void 0 ) createNewLexicalScope = true; if ( node === void 0 ) node = this.startNode(); node.body = []; this.expect(types$1.braceL); if (createNewLexicalScope) { this.enterScope(0); } while (this.type !== types$1.braceR) { var stmt = this.parseStatement(null); node.body.push(stmt); } if (exitStrict) { this.strict = false; } this.next(); if (createNewLexicalScope) { this.exitScope(); } return this.finishNode(node, "BlockStatement") }; // Parse a regular `for` loop. The disambiguation code in // `parseStatement` will already have parsed the init statement or // expression. pp$8.parseFor = function(node, init) { node.init = init; this.expect(types$1.semi); node.test = this.type === types$1.semi ? null : this.parseExpression(); this.expect(types$1.semi); node.update = this.type === types$1.parenR ? null : this.parseExpression(); this.expect(types$1.parenR); node.body = this.parseStatement("for"); this.exitScope(); this.labels.pop(); return this.finishNode(node, "ForStatement") }; // Parse a `for`/`in` and `for`/`of` loop, which are almost // same from parser's perspective. pp$8.parseForIn = function(node, init) { var isForIn = this.type === types$1._in; this.next(); if ( init.type === "VariableDeclaration" && init.declarations[0].init != null && ( !isForIn || this.options.ecmaVersion < 8 || this.strict || init.kind !== "var" || init.declarations[0].id.type !== "Identifier" ) ) { this.raise( init.start, ((isForIn ? "for-in" : "for-of") + " loop variable declaration may not have an initializer") ); } node.left = init; node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign(); this.expect(types$1.parenR); node.body = this.parseStatement("for"); this.exitScope(); this.labels.pop(); return this.finishNode(node, isForIn ? "ForInStatement" : "ForOfStatement") }; // Parse a list of variable declarations. pp$8.parseVar = function(node, isFor, kind, allowMissingInitializer) { node.declarations = []; node.kind = kind; for (;;) { var decl = this.startNode(); this.parseVarId(decl, kind); if (this.eat(types$1.eq)) { decl.init = this.parseMaybeAssign(isFor); } else if (!allowMissingInitializer && kind === "const" && !(this.type === types$1._in || (this.options.ecmaVersion >= 6 && this.isContextual("of")))) { this.unexpected(); } else if (!allowMissingInitializer && (kind === "using" || kind === "await using") && this.options.ecmaVersion >= 17 && this.type !== types$1._in && !this.isContextual("of")) { this.raise(this.lastTokEnd, ("Missing initializer in " + kind + " declaration")); } else if (!allowMissingInitializer && decl.id.type !== "Identifier" && !(isFor && (this.type === types$1._in || this.isContextual("of")))) { this.raise(this.lastTokEnd, "Complex binding patterns require an initialization value"); } else { decl.init = null; } node.declarations.push(this.finishNode(decl, "VariableDeclarator")); if (!this.eat(types$1.comma)) { break } } return node }; pp$8.parseVarId = function(decl, kind) { decl.id = kind === "using" || kind === "await using" ? this.parseIdent() : this.parseBindingAtom(); this.checkLValPattern(decl.id, kind === "var" ? BIND_VAR : BIND_LEXICAL, false); }; var FUNC_STATEMENT = 1, FUNC_HANGING_STATEMENT = 2, FUNC_NULLABLE_ID = 4; // Parse a function declaration or literal (depending on the // `statement & FUNC_STATEMENT`). // Remove `allowExpressionBody` for 7.0.0, as it is only called with false pp$8.parseFunction = function(node, statement, allowExpressionBody, isAsync, forInit) { this.initFunction(node); if (this.options.ecmaVersion >= 9 || this.options.ecmaVersion >= 6 && !isAsync) { if (this.type === types$1.star && (statement & FUNC_HANGING_STATEMENT)) { this.unexpected(); } node.generator = this.eat(types$1.star); } if (this.options.ecmaVersion >= 8) { node.async = !!isAsync; } if (statement & FUNC_STATEMENT) { node.id = (statement & FUNC_NULLABLE_ID) && this.type !== types$1.name ? null : this.parseIdent(); if (node.id && !(statement & FUNC_HANGING_STATEMENT)) // If it is a regular function declaration in sloppy mode, then it is // subject to Annex B semantics (BIND_FUNCTION). Otherwise, the binding // mode depends on properties of the current scope (see // treatFunctionsAsVar). { this.checkLValSimple(node.id, (this.strict || node.generator || node.async) ? this.treatFunctionsAsVar ? BIND_VAR : BIND_LEXICAL : BIND_FUNCTION); } } var oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos; this.yieldPos = 0; this.awaitPos = 0; this.awaitIdentPos = 0; this.enterScope(functionFlags(node.async, node.generator)); if (!(statement & FUNC_STATEMENT)) { node.id = this.type === types$1.name ? this.parseIdent() : null; } this.parseFunctionParams(node); this.parseFunctionBody(node, allowExpressionBody, false, forInit); this.yieldPos = oldYieldPos; this.awaitPos = oldAwaitPos; this.awaitIdentPos = oldAwaitIdentPos; return this.finishNode(node, (statement & FUNC_STATEMENT) ? "FunctionDeclaration" : "FunctionExpression") }; pp$8.parseFunctionParams = function(node) { this.expect(types$1.parenL); node.params = this.parseBindingList(types$1.parenR, false, this.options.ecmaVersion >= 8); this.checkYieldAwaitInDefaultParams(); }; // Parse a class declaration or literal (depending on the // `isStatement` parameter). pp$8.parseClass = function(node, isStatement) { this.next(); // ecma-262 14.6 Class Definitions // A class definition is always strict mode code. var oldStrict = this.strict; this.strict = true; this.parseClassId(node, isStatement); this.parseClassSuper(node); var privateNameMap = this.enterClassBody(); var classBody = this.startNode(); var hadConstructor = false; classBody.body = []; this.expect(types$1.braceL); while (this.type !== types$1.braceR) { var element = this.parseClassElement(node.superClass !== null); if (element) { classBody.body.push(element); if (element.type === "MethodDefinition" && element.kind === "constructor") { if (hadConstructor) { this.raiseRecoverable(element.start, "Duplicate constructor in the same class"); } hadConstructor = true; } else if (element.key && element.key.type === "PrivateIdentifier" && isPrivateNameConflicted(privateNameMap, element)) { this.raiseRecoverable(element.key.start, ("Identifier '#" + (element.key.name) + "' has already been declared")); } } } this.strict = oldStrict; this.next(); node.body = this.finishNode(classBody, "ClassBody"); this.exitClassBody(); return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") }; pp$8.parseClassElement = function(constructorAllowsSuper) { if (this.eat(types$1.semi)) { return null } var ecmaVersion = this.options.ecmaVersion; var node = this.startNode(); var keyName = ""; var isGenerator = false; var isAsync = false; var kind = "method"; var isStatic = false; if (this.eatContextual("static")) { // Parse static init block if (ecmaVersion >= 13 && this.eat(types$1.braceL)) { this.parseClassStaticBlock(node); return node } if (this.isClassElementNameStart() || this.type === types$1.star) { isStatic = true; } else { keyName = "static"; } } node.static = isStatic; if (!keyName && ecmaVersion >= 8 && this.eatContextual("async")) { if ((this.isClassElementNameStart() || this.type === types$1.star) && !this.canInsertSemicolon()) { isAsync = true; } else { keyName = "async"; } } if (!keyName && (ecmaVersion >= 9 || !isAsync) && this.eat(types$1.star)) { isGenerator = true; } if (!keyName && !isAsync && !isGenerator) { var lastValue = this.value; if (this.eatContextual("get") || this.eatContextual("set")) { if (this.isClassElementNameStart()) { kind = lastValue; } else { keyName = lastValue; } } } // Parse element name if (keyName) { // 'async', 'get', 'set', or 'static' were not a keyword contextually. // The last token is any of those. Make it the element name. node.computed = false; node.key = this.startNodeAt(this.lastTokStart, this.lastTokStartLoc); node.key.name = keyName; this.finishNode(node.key, "Identifier"); } else { this.parseClassElementName(node); } // Parse element value if (ecmaVersion < 13 || this.type === types$1.parenL || kind !== "method" || isGenerator || isAsync) { var isConstructor = !node.static && checkKeyName(node, "constructor"); var allowsDirectSuper = isConstructor && constructorAllowsSuper; // Couldn't move this check into the 'parseClassMethod' method for backward compatibility. if (isConstructor && kind !== "method") { this.raise(node.key.start, "Constructor can't have get/set modifier"); } node.kind = isConstructor ? "constructor" : kind; this.parseClassMethod(node, isGenerator, isAsync, allowsDirectSuper); } else { this.parseClassField(node); } return node }; pp$8.isClassElementNameStart = function() { return ( this.type === types$1.name || this.type === types$1.privateId || this.type === types$1.num || this.type === types$1.string || this.type === types$1.bracketL || this.type.keyword ) }; pp$8.parseClassElementName = function(element) { if (this.type === types$1.privateId) { if (this.value === "constructor") { this.raise(this.start, "Classes can't have an element named '#constructor'"); } element.computed = false; element.key = this.parsePrivateIdent(); } else { this.parsePropertyName(element); } }; pp$8.parseClassMethod = function(method, isGenerator, isAsync, allowsDirectSuper) { // Check key and flags var key = method.key; if (method.kind === "constructor") { if (isGenerator) { this.raise(key.start, "Constructor can't be a generator"); } if (isAsync) { this.raise(key.start, "Constructor can't be an async method"); } } else if (method.static && checkKeyName(method, "prototype")) { this.raise(key.start, "Classes may not have a static property named prototype"); } // Parse value var value = method.value = this.parseMethod(isGenerator, isAsync, allowsDirectSuper); // Check value if (method.kind === "get" && value.params.length !== 0) { this.raiseRecoverable(value.start, "getter should have no params"); } if (method.kind === "set" && value.params.length !== 1) { this.raiseRecoverable(value.start, "setter should have exactly one param"); } if (method.kind === "set" && value.params[0].type === "RestElement") { this.raiseRecoverable(value.params[0].start, "Setter cannot use rest params"); } return this.finishNode(method, "MethodDefinition") }; pp$8.parseClassField = function(field) { if (checkKeyName(field, "constructor")) { this.raise(field.key.start, "Classes can't have a field named 'constructor'"); } else if (field.static && checkKeyName(field, "prototype")) { this.raise(field.key.start, "Classes can't have a static field named 'prototype'"); } if (this.eat(types$1.eq)) { // To raise SyntaxError if 'arguments' exists in the initializer. this.enterScope(SCOPE_CLASS_FIELD_INIT | SCOPE_SUPER); field.value = this.parseMaybeAssign(); this.exitScope(); } else { field.value = null; } this.semicolon(); return this.finishNode(field, "PropertyDefinition") }; pp$8.parseClassStaticBlock = function(node) { node.body = []; var oldLabels = this.labels; this.labels = []; this.enterScope(SCOPE_CLASS_STATIC_BLOCK | SCOPE_SUPER); while (this.type !== types$1.braceR) { var stmt = this.parseStatement(null); node.body.push(stmt); } this.next(); this.exitScope(); this.labels = oldLabels; return this.finishNode(node, "StaticBlock") }; pp$8.parseClassId = function(node, isStatement) { if (this.type === types$1.name) { node.id = this.parseIdent(); if (isStatement) { this.checkLValSimple(node.id, BIND_LEXICAL, false); } } else { if (isStatement === true) { this.unexpected(); } node.id = null; } }; pp$8.parseClassSuper = function(node) { node.superClass = this.eat(types$1._extends) ? this.parseExprSubscripts(null, false) : null; }; pp$8.enterClassBody = function() { var element = {declared: Object.create(null), used: []}; this.privateNameStack.push(element); return element.declared }; pp$8.exitClassBody = function() { var ref = this.privateNameStack.pop(); var declared = ref.declared; var used = ref.used; if (!this.options.checkPrivateFields) { return } var len = this.privateNameStack.length; var parent = len === 0 ? null : this.privateNameStack[len - 1]; for (var i = 0; i < used.length; ++i) { var id = used[i]; if (!hasOwn(declared, id.name)) { if (parent) { parent.used.push(id); } else { this.raiseRecoverable(id.start, ("Private field '#" + (id.name) + "' must be declared in an enclosing class")); } } } }; function isPrivateNameConflicted(privateNameMap, element) { var name = element.key.name; var curr = privateNameMap[name]; var next = "true"; if (element.type === "MethodDefinition" && (element.kind === "get" || element.kind === "set")) { next = (element.static ? "s" : "i") + element.kind; } // `class { get #a(){}; static set #a(_){} }` is also conflict. if ( curr === "iget" && next === "iset" || curr === "iset" && next === "iget" || curr === "sget" && next === "sset" || curr === "sset" && next === "sget" ) { privateNameMap[name] = "true"; return false } else if (!curr) { privateNameMap[name] = next; return false } else { return true } } function checkKeyName(node, name) { var computed = node.computed; var key = node.key; return !computed && ( key.type === "Identifier" && key.name === name || key.type === "Literal" && key.value === name ) } // Parses module export declaration. pp$8.parseExportAllDeclaration = function(node, exports$1) { if (this.options.ecmaVersion >= 11) { if (this.eatContextual("as")) { node.exported = this.parseModuleExportName(); this.checkExport(exports$1, node.exported, this.lastTokStart); } else { node.exported = null; } } this.expectContextual("from"); if (this.type !== types$1.string) { this.unexpected(); } node.source = this.parseExprAtom(); if (this.options.ecmaVersion >= 16) { node.attributes = this.parseWithClause(); } this.semicolon(); return this.finishNode(node, "ExportAllDeclaration") }; pp$8.parseExport = function(node, exports$1) { this.next(); // export * from '...' if (this.eat(types$1.star)) { return this.parseExportAllDeclaration(node, exports$1) } if (this.eat(types$1._default)) { // export default ... this.checkExport(exports$1, "default", this.lastTokStart); node.declaration = this.parseExportDefaultDeclaration(); return this.finishNode(node, "ExportDefaultDeclaration") } // export var|const|let|function|class ... if (this.shouldParseExportStatement()) { node.declaration = this.parseExportDeclaration(node); if (node.declaration.type === "VariableDeclaration") { this.checkVariableExport(exports$1, node.declaration.declarations); } else { this.checkExport(exports$1, node.declaration.id, node.declaration.id.start); } node.specifiers = []; node.source = null; if (this.options.ecmaVersion >= 16) { node.attributes = []; } } else { // export { x, y as z } [from '...'] node.declaration = null; node.specifiers = this.parseExportSpecifiers(exports$1); if (this.eatContextual("from")) { if (this.type !== types$1.string) { this.unexpected(); } node.source = this.parseExprAtom(); if (this.options.ecmaVersion >= 16) { node.attributes = this.parseWithClause(); } } else { for (var i = 0, list = node.specifiers; i < list.length; i += 1) { // check for keywords used as local names var spec = list[i]; this.checkUnreserved(spec.local); // check if export is defined this.checkLocalExport(spec.local); if (spec.local.type === "Literal") { this.raise(spec.local.start, "A string literal cannot be used as an exported binding without `from`."); } } node.source = null; if (this.options.ecmaVersion >= 16) { node.attributes = []; } } this.semicolon(); } return this.finishNode(node, "ExportNamedDeclaration") }; pp$8.parseExportDeclaration = function(node) { return this.parseStatement(null) }; pp$8.parseExportDefaultDeclaration = function() { var isAsync; if (this.type === types$1._function || (isAsync = this.isAsyncFunction())) { var fNode = this.startNode(); this.next(); if (isAsync) { this.next(); } return this.parseFunction(fNode, FUNC_STATEMENT | FUNC_NULLABLE_ID, false, isAsync) } else if (this.type === types$1._class) { var cNode = this.startNode(); return this.parseClass(cNode, "nullableID") } else { var declaration = this.parseMaybeAssign(); this.semicolon(); return declaration } }; pp$8.checkExport = function(exports$1, name, pos) { if (!exports$1) { return } if (typeof name !== "string") { name = name.type === "Identifier" ? name.name : name.value; } if (hasOwn(exports$1, name)) { this.raiseRecoverable(pos, "Duplicate export '" + name + "'"); } exports$1[name] = true; }; pp$8.checkPatternExport = function(exports$1, pat) { var type = pat.type; if (type === "Identifier") { this.checkExport(exports$1, pat, pat.start); } else if (type === "ObjectPattern") { for (var i = 0, list = pat.properties; i < list.length; i += 1) { var prop = list[i]; this.checkPatternExport(exports$1, prop); } } else if (type === "ArrayPattern") { for (var i$1 = 0, list$1 = pat.elements; i$1 < list$1.length; i$1 += 1) { var elt = list$1[i$1]; if (elt) { this.checkPatternExport(exports$1, elt); } } } else if (type === "Property") { this.checkPatternExport(exports$1, pat.value); } else if (type === "AssignmentPattern") { this.checkPatternExport(exports$1, pat.left); } else if (type === "RestElement") { this.checkPatternExport(exports$1, pat.argument); } }; pp$8.checkVariableExport = function(exports$1, decls) { if (!exports$1) { return } for (var i = 0, list = decls; i < list.length; i += 1) { var decl = list[i]; this.checkPatternExport(exports$1, decl.id); } }; pp$8.shouldParseExportStatement = function() { return this.type.keyword === "var" || this.type.keyword === "const" || this.type.keyword === "class" || this.type.keyword === "function" || this.isLet() || this.isAsyncFunction() }; // Parses a comma-separated list of module exports. pp$8.parseExportSpecifier = function(exports$1) { var node = this.startNode(); node.local = this.parseModuleExportName(); node.exported = this.eatContextual("as") ? this.parseModuleExportName() : node.local; this.checkExport( exports$1, node.exported, node.exported.start ); return this.finishNode(node, "ExportSpecifier") }; pp$8.parseExportSpecifiers = function(exports$1) { var nodes = [], first = true; // export { x, y as z } [from '...'] this.expect(types$1.braceL); while (!this.eat(types$1.braceR)) { if (!first) { this.expect(types$1.comma); if (this.afterTrailingComma(types$1.braceR)) { break } } else { first = false; } nodes.push(this.parseExportSpecifier(exports$1)); } return nodes }; // Parses import declaration. pp$8.parseImport = function(node) { this.next(); // import '...' if (this.type === types$1.string) { node.specifiers = empty$1; node.source = this.parseExprAtom(); } else { node.specifiers = this.parseImportSpecifiers(); this.expectContextual("from"); node.source = this.type === types$1.string ? this.parseExprAtom() : this.unexpected(); } if (this.options.ecmaVersion >= 16) { node.attributes = this.parseWithClause(); } this.semicolon(); return this.finishNode(node, "ImportDeclaration") }; // Parses a comma-separated list of module imports. pp$8.parseImportSpecifier = function() { var node = this.startNode(); node.imported = this.parseModuleExportName(); if (this.eatContextual("as")) { node.local = this.parseIdent(); } else { this.checkUnreserved(node.imported); node.local = node.imported; } this.checkLValSimple(node.local, BIND_LEXICAL); return this.finishNode(node, "ImportSpecifier") }; pp$8.parseImportDefaultSpecifier = function() { // import defaultObj, { x, y as z } from '...' var node = this.startNode(); node.local = this.parseIdent(); this.checkLValSimple(node.local, BIND_LEXICAL); return this.finishNode(node, "ImportDefaultSpecifier") }; pp$8.parseImportNamespaceSpecifier = function() { var node = this.startNode(); this.next(); this.expectContextual("as"); node.local = this.parseIdent(); this.checkLValSimple(node.local, BIND_LEXICAL); return this.finishNode(node, "ImportNamespaceSpecifier") }; pp$8.parseImportSpecifiers = function() { var nodes = [], first = true; if (this.type === types$1.name) { nodes.push(this.parseImportDefaultSpecifier()); if (!this.eat(types$1.comma)) { return nodes } } if (this.type === types$1.star) { nodes.push(this.parseImportNamespaceSpecifier()); return nodes } this.expect(types$1.braceL); while (!this.eat(types$1.braceR)) { if (!first) { this.expect(types$1.comma); if (this.afterTrailingComma(types$1.braceR)) { break } } else { first = false; } nodes.push(this.parseImportSpecifier()); } return nodes }; pp$8.parseWithClause = function() { var nodes = []; if (!this.eat(types$1._with)) { return nodes } this.expect(types$1.braceL); var attributeKeys = {}; var first = true; while (!this.eat(types$1.braceR)) { if (!first) { this.expect(types$1.comma); if (this.afterTrailingComma(types$1.braceR)) { break } } else { first = false; } var attr = this.parseImportAttribute(); var keyName = attr.key.type === "Identifier" ? attr.key.name : attr.key.value; if (hasOwn(attributeKeys, keyName)) { this.raiseRecoverable(attr.key.start, "Duplicate attribute key '" + keyName + "'"); } attributeKeys[keyName] = true; nodes.push(attr); } return nodes }; pp$8.parseImportAttribute = function() { var node = this.startNode(); node.key = this.type === types$1.string ? this.parseExprAtom() : this.parseIdent(this.options.allowReserved !== "never"); this.expect(types$1.colon); if (this.type !== types$1.string) { this.unexpected(); } node.value = this.parseExprAtom(); return this.finishNode(node, "ImportAttribute") }; pp$8.parseModuleExportName = function() { if (this.options.ecmaVersion >= 13 && this.type === types$1.string) { var stringLiteral = this.parseLiteral(this.value); if (loneSurrogate.test(stringLiteral.value)) { this.raise(stringLiteral.start, "An export name cannot include a lone surrogate."); } return stringLiteral } return this.parseIdent(true) }; // Set `ExpressionStatement#directive` property for directive prologues. pp$8.adaptDirectivePrologue = function(statements) { for (var i = 0; i < statements.length && this.isDirectiveCandidate(statements[i]); ++i) { statements[i].directive = statements[i].expression.raw.slice(1, -1); } }; pp$8.isDirectiveCandidate = function(statement) { return ( this.options.ecmaVersion >= 5 && statement.type === "ExpressionStatement" && statement.expression.type === "Literal" && typeof statement.expression.value === "string" && // Reject parenthesized strings. (this.input[statement.start] === "\"" || this.input[statement.start] === "'") ) }; var pp$7 = Parser.prototype; // Convert existing expression atom to assignable pattern // if possible. pp$7.toAssignable = function(node, isBinding, refDestructuringErrors) { if (this.options.ecmaVersion >= 6 && node) { switch (node.type) { case "Identifier": if (this.inAsync && node.name === "await") { this.raise(node.start, "Cannot use 'await' as identifier inside an async function"); } break case "ObjectPattern": case "ArrayPattern": case "AssignmentPattern": case "RestElement": break case "ObjectExpression": node.type = "ObjectPattern"; if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } for (var i = 0, list = node.properties; i < list.length; i += 1) { var prop = list[i]; this.toAssignable(prop, isBinding); // Early error: // AssignmentRestProperty[Yield, Await] : // `...` DestructuringAssignmentTarget[Yield, Await] // // It is a Syntax Error if |DestructuringAssignmentTarget| is an |ArrayLiteral| or an |ObjectLiteral|. if ( prop.type === "RestElement" && (prop.argument.type === "ArrayPattern" || prop.argument.type === "ObjectPattern") ) { this.raise(prop.argument.start, "Unexpected token"); } } break case "Property": // AssignmentProperty has type === "Property" if (node.kind !== "init") { this.raise(node.key.start, "Object pattern can't contain getter or setter"); } this.toAssignable(node.value, isBinding); break case "ArrayExpression": node.type = "ArrayPattern"; if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } this.toAssignableList(node.elements, isBinding); break case "SpreadElement": node.type = "RestElement"; this.toAssignable(node.argument, isBinding); if (node.argument.type === "AssignmentPattern") { this.raise(node.argument.start, "Rest elements cannot have a default value"); } break case "AssignmentExpression": if (node.operator !== "=") { this.raise(node.left.end, "Only '=' operator can be used for specifying default value."); } node.type = "AssignmentPattern"; delete node.operator; this.toAssignable(node.left, isBinding); break case "ParenthesizedExpression": this.toAssignable(node.expression, isBinding, refDestructuringErrors); break case "ChainExpression": this.raiseRecoverable(node.start, "Optional chaining cannot appear in left-hand side"); break case "MemberExpression": if (!isBinding) { break } default: this.raise(node.start, "Assigning to rvalue"); } } else if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } return node }; // Convert list of expression atoms to binding list. pp$7.toAssignableList = function(exprList, isBinding) { var end = exprList.length; for (var i = 0; i < end; i++) { var elt = exprList[i]; if (elt) { this.toAssignable(elt, isBinding); } } if (end) { var last = exprList[end - 1]; if (this.options.ecmaVersion === 6 && isBinding && last && last.type === "RestElement" && last.argument.type !== "Identifier") { this.unexpected(last.argument.start); } } return exprList }; // Parses spread element. pp$7.parseSpread = function(refDestructuringErrors) { var node = this.startNode(); this.next(); node.argument = this.parseMaybeAssign(false, refDestructuringErrors); return this.finishNode(node, "SpreadElement") }; pp$7.parseRestBinding = function() { var node = this.startNode(); this.next(); // RestElement inside of a function parameter must be an identifier if (this.options.ecmaVersion === 6 && this.type !== types$1.name) { this.unexpected(); } node.argument = this.parseBindingAtom(); return this.finishNode(node, "RestElement") }; // Parses lvalue (assignable) atom. pp$7.parseBindingAtom = function() { if (this.options.ecmaVersion >= 6) { switch (this.type) { case types$1.bracketL: var node = this.startNode(); this.next(); node.elements = this.parseBindingList(types$1.bracketR, true, true); return this.finishNode(node, "ArrayPattern") case types$1.braceL: return this.parseObj(true) } } return this.parseIdent() }; pp$7.parseBindingList = function(close, allowEmpty, allowTrailingComma, allowModifiers) { var elts = [], first = true; while (!this.eat(close)) { if (first) { first = false; } else { this.expect(types$1.comma); } if (allowEmpty && this.type === types$1.comma) { elts.push(null); } else if (allowTrailingComma && this.afterTrailingComma(close)) { break } else if (this.type === types$1.ellipsis) { var rest = this.parseRestBinding(); this.parseBindingListItem(rest); elts.push(rest); if (this.type === types$1.comma) { this.raiseRecoverable(this.start, "Comma is not permitted after the rest element"); } this.expect(close); break } else { elts.push(this.parseAssignableListItem(allowModifiers)); } } return elts }; pp$7.parseAssignableListItem = function(allowModifiers) { var elem = this.parseMaybeDefault(this.start, this.startLoc); this.parseBindingListItem(elem); return elem }; pp$7.parseBindingListItem = function(param) { return param }; // Parses assignment pattern around given atom if possible. pp$7.parseMaybeDefault = function(startPos, startLoc, left) { left = left || this.parseBindingAtom(); if (this.options.ecmaVersion < 6 || !this.eat(types$1.eq)) { return left } var node = this.startNodeAt(startPos, startLoc); node.left = left; node.right = this.parseMaybeAssign(); return this.finishNode(node, "AssignmentPattern") }; // The following three functions all verify that a node is an lvalue — // something that can be bound, or assigned to. In order to do so, they perform // a variety of checks: // // - Check that none of the bound/assigned-to identifiers are reserved words. // - Record name declarations for bindings in the appropriate scope. // - Check duplicate argument names, if checkClashes is set. // // If a complex binding pattern is encountered (e.g., object and array // destructuring), the entire pattern is recursively checked. // // There are three versions of checkLVal*() appropriate for different // circumstances: // // - checkLValSimple() shall be used if the syntactic construct supports // nothing other than identifiers and member expressions. Parenthesized // expressions are also correctly handled. This is generally appropriate for // constructs for which the spec says // // > It is a Syntax Error if AssignmentTargetType of [the production] is not // > simple. // // It is also appropriate for checking if an identifier is valid and not // defined elsewhere, like import declarations or function/class identifiers. // // Examples where this is used include: // a += …; // import a from '…'; // where a is the node to be checked. // // - checkLValPattern() shall be used if the syntactic construct supports // anything checkLValSimple() supports, as well as object and array // destructuring patterns. This is generally appropriate for constructs for // which the spec says // // > It is a Syntax Error if [the production] is neither an ObjectLiteral nor // > an ArrayLiteral and AssignmentTargetType of [the production] is not // > simple. // // Examples where this is used include: // (a = …); // const a = …; // try { … } catch (a) { … } // where a is the node to be checked. // // - checkLValInnerPattern() shall be used if the syntactic construct supports // anything checkLValPattern() supports, as well as default assignment // patterns, rest elements, and other constructs that may appear within an // object or array destructuring pattern. // // As a special case, function parameters also use checkLValInnerPattern(), // as they also support defaults and rest constructs. // // These functions deliberately support both assignment and binding constructs, // as the logic for both is exceedingly similar. If the node is the target of // an assignment, then bindingType should be set to BIND_NONE. Otherwise, it // should be set to the appropriate BIND_* constant, like BIND_VAR or // BIND_LEXICAL. // // If the function is called with a non-BIND_NONE bindingType, then // additionally a checkClashes object may be specified to allow checking for // duplicate argument names. checkClashes is ignored if the provided construct // is an assignment (i.e., bindingType is BIND_NONE). pp$7.checkLValSimple = function(expr, bindingType, checkClashes) { if ( bindingType === void 0 ) bindingType = BIND_NONE; var isBind = bindingType !== BIND_NONE; switch (expr.type) { case "Identifier": if (this.strict && this.reservedWordsStrictBind.test(expr.name)) { this.raiseRecoverable(expr.start, (isBind ? "Binding " : "Assigning to ") + expr.name + " in strict mode"); } if (isBind) { if (bindingType === BIND_LEXICAL && expr.name === "let") { this.raiseRecoverable(expr.start, "let is disallowed as a lexically bound name"); } if (checkClashes) { if (hasOwn(checkClashes, expr.name)) { this.raiseRecoverable(expr.start, "Argument name clash"); } checkClashes[expr.name] = true; } if (bindingType !== BIND_OUTSIDE) { this.declareName(expr.name, bindingType, expr.start); } } break case "ChainExpression": this.raiseRecoverable(expr.start, "Optional chaining cannot appear in left-hand side"); break case "MemberExpression": if (isBind) { this.raiseRecoverable(expr.start, "Binding member expression"); } break case "ParenthesizedExpression": if (isBind) { this.raiseRecoverable(expr.start, "Binding parenthesized expression"); } return this.checkLValSimple(expr.expression, bindingType, checkClashes) default: this.raise(expr.start, (isBind ? "Binding" : "Assigning to") + " rvalue"); } }; pp$7.checkLValPattern = function(expr, bindingType, checkClashes) { if ( bindingType === void 0 ) bindingType = BIND_NONE; switch (expr.type) { case "ObjectPattern": for (var i = 0, list = expr.properties; i < list.length; i += 1) { var prop = list[i]; this.checkLValInnerPattern(prop, bindingType, checkClashes); } break case "ArrayPattern": for (var i$1 = 0, list$1 = expr.elements; i$1 < list$1.length; i$1 += 1) { var elem = list$1[i$1]; if (elem) { this.checkLValInnerPattern(elem, bindingType, checkClashes); } } break default: this.checkLValSimple(expr, bindingType, checkClashes); } }; pp$7.checkLValInnerPattern = function(expr, bindingType, checkClashes) { if ( bindingType === void 0 ) bindingType = BIND_NONE; switch (expr.type) { case "Property": // AssignmentProperty has type === "Property" this.checkLValInnerPattern(expr.value, bindingType, checkClashes); break case "AssignmentPattern": this.checkLValPattern(expr.left, bindingType, checkClashes); break case "RestElement": this.checkLValPattern(expr.argument, bindingType, checkClashes); break default: this.checkLValPattern(expr, bindingType, checkClashes); } }; // The algorithm used to determine whether a regexp can appear at a // given point in the program is loosely based on sweet.js' approach. // See https://github.com/mozilla/sweet.js/wiki/design var TokContext = function TokContext(token, isExpr, preserveSpace, override, generator) { this.token = token; this.isExpr = !!isExpr; this.preserveSpace = !!preserveSpace; this.override = override; this.generator = !!generator; }; var types = { b_stat: new TokContext("{", false), b_expr: new TokContext("{", true), b_tmpl: new TokContext("${", false), p_stat: new TokContext("(", false), p_expr: new TokContext("(", true), q_tmpl: new TokContext("`", true, true, function (p) { return p.tryReadTemplateToken(); }), f_stat: new TokContext("function", false), f_expr: new TokContext("function", true), f_expr_gen: new TokContext("function", true, false, null, true), f_gen: new TokContext("function", false, false, null, true) }; var pp$6 = Parser.prototype; pp$6.initialContext = function() { return [types.b_stat] }; pp$6.curContext = function() { return this.context[this.context.length - 1] }; pp$6.braceIsBlock = function(prevType) { var parent = this.curContext(); if (parent === types.f_expr || parent === types.f_stat) { return true } if (prevType === types$1.colon && (parent === types.b_stat || parent === types.b_expr)) { return !parent.isExpr } // The check for `tt.name && exprAllowed` detects whether we are // after a `yield` or `of` construct. See the `updateContext` for // `tt.name`. if (prevType === types$1._return || prevType === types$1.name && this.exprAllowed) { return lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) } if (prevType === types$1._else || prevType === types$1.semi || prevType === types$1.eof || prevType === types$1.parenR || prevType === types$1.arrow) { return true } if (prevType === types$1.braceL) { return parent === types.b_stat } if (prevType === types$1._var || prevType === types$1._const || prevType === types$1.name) { return false } return !this.exprAllowed }; pp$6.inGeneratorContext = function() { for (var i = this.context.length - 1; i >= 1; i--) { var context = this.context[i]; if (context.token === "function") { return context.generator } } return false }; pp$6.updateContext = function(prevType) { var update, type = this.type; if (type.keyword && prevType === types$1.dot) { this.exprAllowed = false; } else if (update = type.updateContext) { update.call(this, prevType); } else { this.exprAllowed = type.beforeExpr; } }; // Used to handle edge cases when token context could not be inferred correctly during tokenization phase pp$6.overrideContext = function(tokenCtx) { if (this.curContext() !== tokenCtx) { this.context[this.context.length - 1] = tokenCtx; } }; // Token-specific context update code types$1.parenR.updateContext = types$1.braceR.updateContext = function() { if (this.context.length === 1) { this.exprAllowed = true; return } var out = this.context.pop(); if (out === types.b_stat && this.curContext().token === "function") { out = this.context.pop(); } this.exprAllowed = !out.isExpr; }; types$1.braceL.updateContext = function(prevType) { this.context.push(this.braceIsBlock(prevType) ? types.b_stat : types.b_expr); this.exprAllowed = true; }; types$1.dollarBraceL.updateContext = function() { this.context.push(types.b_tmpl); this.exprAllowed = true; }; types$1.parenL.updateContext = function(prevType) { var statementParens = prevType === types$1._if || prevType === types$1._for || prevType === types$1._with || prevType === types$1._while; this.context.push(statementParens ? types.p_stat : types.p_expr); this.exprAllowed = true; }; types$1.incDec.updateContext = function() { // tokExprAllowed stays unchanged }; types$1._function.updateContext = types$1._class.updateContext = function(prevType) { if (prevType.beforeExpr && prevType !== types$1._else && !(prevType === types$1.semi && this.curContext() !== types.p_stat) && !(prevType === types$1._return && lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) && !((prevType === types$1.colon || prevType === types$1.braceL) && this.curContext() === types.b_stat)) { this.context.push(types.f_expr); } else { this.context.push(types.f_stat); } this.exprAllowed = false; }; types$1.colon.updateContext = function() { if (this.curContext().token === "function") { this.context.pop(); } this.exprAllowed = true; }; types$1.backQuote.updateContext = function() { if (this.curContext() === types.q_tmpl) { this.context.pop(); } else { this.context.push(types.q_tmpl); } this.exprAllowed = false; }; types$1.star.updateContext = function(prevType) { if (prevType === types$1._function) { var index = this.context.length - 1; if (this.context[index] === types.f_expr) { this.context[index] = types.f_expr_gen; } else { this.context[index] = types.f_gen; } } this.exprAllowed = true; }; types$1.name.updateContext = function(prevType) { var allowed = false; if (this.options.ecmaVersion >= 6 && prevType !== types$1.dot) { if (this.value === "of" && !this.exprAllowed || this.value === "yield" && this.inGeneratorContext()) { allowed = true; } } this.exprAllowed = allowed; }; // A recursive descent parser operates by defining functions for all // syntactic elements, and recursively calling those, each function // advancing the input stream and returning an AST node. Precedence // of constructs (for example, the fact that `!x[1]` means `!(x[1])` // instead of `(!x)[1]` is handled by the fact that the parser // function that parses unary prefix operators is called first, and // in turn calls the function that parses `[]` subscripts — that // way, it'll receive the node for `x[1]` already parsed, and wraps // *that* in the unary operator node. // // Acorn uses an [operator precedence parser][opp] to handle binary // operator precedence, because it is much more compact than using // the technique outlined above, which uses different, nesting // functions to specify precedence, for all of the ten binary // precedence levels that JavaScript defines. // // [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser var pp$5 = Parser.prototype; // Check if property name clashes with already added. // Object/class getters and setters are not allowed to clash — // either with each other or with an init property — and in // strict mode, init properties are also not allowed to be repeated. pp$5.checkPropClash = function(prop, propHash, refDestructuringErrors) { if (this.options.ecmaVersion >= 9 && prop.type === "SpreadElement") { return } if (this.options.ecmaVersion >= 6 && (prop.computed || prop.method || prop.shorthand)) { return } var key = prop.key; var name; switch (key.type) { case "Identifier": name = key.name; break case "Literal": name = String(key.value); break default: return } var kind = prop.kind; if (this.options.ecmaVersion >= 6) { if (name === "__proto__" && kind === "init") { if (propHash.proto) { if (refDestructuringErrors) { if (refDestructuringErrors.doubleProto < 0) { refDestructuringErrors.doubleProto = key.start; } } else { this.raiseRecoverable(key.start, "Redefinition of __proto__ property"); } } propHash.proto = true; } return } name = "$" + name; var other = propHash[name]; if (other) { var redefinition; if (kind === "init") { redefinition = this.strict && other.init || other.get || other.set; } else { redefinition = other.init || other[kind]; } if (redefinition) { this.raiseRecoverable(key.start, "Redefinition of property"); } } else { other = propHash[name] = { init: false, get: false, set: false }; } other[kind] = true; }; // ### Expression parsing // These nest, from the most general expression type at the top to // 'atomic', nondivisible expression types at the bottom. Most of // the functions will simply let the function(s) below them parse, // and, *if* the syntactic construct they handle is present, wrap // the AST node that the inner parser gave them in another node. // Parse a full expression. The optional arguments are used to // forbid the `in` operator (in for loops initalization expressions) // and provide reference for storing '=' operator inside shorthand // property assignment in contexts where both object expression // and object pattern might appear (so it's possible to raise // delayed syntax error at correct position). pp$5.parseExpression = function(forInit, refDestructuringErrors) { var startPos = this.start, startLoc = this.startLoc; var expr = this.parseMaybeAssign(forInit, refDestructuringErrors); if (this.type === types$1.comma) { var node = this.startNodeAt(startPos, startLoc); node.expressions = [expr]; while (this.eat(types$1.comma)) { node.expressions.push(this.parseMaybeAssign(forInit, refDestructuringErrors)); } return this.finishNode(node, "SequenceExpression") } return expr }; // Parse an assignment expression. This includes applications of // operators like `+=`. pp$5.parseMaybeAssign = function(forInit, refDestructuringErrors, afterLeftParse) { if (this.isContextual("yield")) { if (this.inGenerator) { return this.parseYield(forInit) } // The tokenizer will assume an expression is allowed after // `yield`, but this isn't that kind of yield else { this.exprAllowed = false; } } var ownDestructuringErrors = false, oldParenAssign = -1, oldTrailingComma = -1, oldDoubleProto = -1; if (refDestructuringErrors) { oldParenAssign = refDestructuringErrors.parenthesizedAssign; oldTrailingComma = refDestructuringErrors.trailingComma; oldDoubleProto = refDestructuringErrors.doubleProto; refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = -1; } else { refDestructuringErrors = new DestructuringErrors; ownDestructuringErrors = true; } var startPos = this.start, startLoc = this.startLoc; if (this.type === types$1.parenL || this.type === types$1.name) { this.potentialArrowAt = this.start; this.potentialArrowInForAwait = forInit === "await"; } var left = this.parseMaybeConditional(forInit, refDestructuringErrors); if (afterLeftParse) { left = afterLeftParse.call(this, left, startPos, startLoc); } if (this.type.isAssign) { var node = this.startNodeAt(startPos, startLoc); node.operator = this.value; if (this.type === types$1.eq) { left = this.toAssignable(left, false, refDestructuringErrors); } if (!ownDestructuringErrors) { refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = refDestructuringErrors.doubleProto = -1; } if (refDestructuringErrors.shorthandAssign >= left.start) { refDestructuringErrors.shorthandAssign = -1; } // reset because shorthand default was used correctly if (this.type === types$1.eq) { this.checkLValPattern(left); } else { this.checkLValSimple(left); } node.left = left; this.next(); node.right = this.parseMaybeAssign(forInit); if (oldDoubleProto > -1) { refDestructuringErrors.doubleProto = oldDoubleProto; } return this.finishNode(node, "AssignmentExpression") } else { if (ownDestructuringErrors) { this.checkExpressionErrors(refDestructuringErrors, true); } } if (oldParenAssign > -1) { refDestructuringErrors.parenthesizedAssign = oldParenAssign; } if (oldTrailingComma > -1) { refDestructuringErrors.trailingComma = oldTrailingComma; } return left }; // Parse a ternary conditional (`?:`) operator. pp$5.parseMaybeConditional = function(forInit, refDestructuringErrors) { var startPos = this.start, startLoc = this.startLoc; var expr = this.parseExprOps(forInit, refDestructuringErrors); if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } if (this.eat(types$1.question)) { var node = this.startNodeAt(startPos, startLoc); node.test = expr; node.consequent = this.parseMaybeAssign(); this.expect(types$1.colon); node.alternate = this.parseMaybeAssign(forInit); return this.finishNode(node, "ConditionalExpression") } return expr }; // Start the precedence parser. pp$5.parseExprOps = function(forInit, refDestructuringErrors) { var startPos = this.start, startLoc = this.startLoc; var expr = this.parseMaybeUnary(refDestructuringErrors, false, false, forInit); if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } return expr.start === startPos && expr.type === "ArrowFunctionExpression" ? expr : this.parseExprOp(expr, startPos, startLoc, -1, forInit) }; // Parse binary operators with the operator precedence parsing // algorithm. `left` is the left-hand side of the operator. // `minPrec` provides context that allows the function to stop and // defer further parser to one of its callers when it encounters an // operator that has a lower precedence than the set it is parsing. pp$5.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, forInit) { var prec = this.type.binop; if (prec != null && (!forInit || this.type !== types$1._in)) { if (prec > minPrec) { var logical = this.type === types$1.logicalOR || this.type === types$1.logicalAND; var coalesce = this.type === types$1.coalesce; if (coalesce) { // Handle the precedence of `tt.coalesce` as equal to the range of logical expressions. // In other words, `node.right` shouldn't contain logical expressions in order to check the mixed error. prec = types$1.logicalAND.binop; } var op = this.value; this.next(); var startPos = this.start, startLoc = this.startLoc; var right = this.parseExprOp(this.parseMaybeUnary(null, false, false, forInit), startPos, startLoc, prec, forInit); var node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical || coalesce); if ((logical && this.type === types$1.coalesce) || (coalesce && (this.type === types$1.logicalOR || this.type === types$1.logicalAND))) { this.raiseRecoverable(this.start, "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses"); } return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, forInit) } } return left }; pp$5.buildBinary = function(startPos, startLoc, left, right, op, logical) { if (right.type === "PrivateIdentifier") { this.raise(right.start, "Private identifier can only be left side of binary expression"); } var node = this.startNodeAt(startPos, startLoc); node.left = left; node.operator = op; node.right = right; return this.finishNode(node, logical ? "LogicalExpression" : "BinaryExpression") }; // Parse unary operators, both prefix and postfix. pp$5.parseMaybeUnary = function(refDestructuringErrors, sawUnary, incDec, forInit) { var startPos = this.start, startLoc = this.startLoc, expr; if (this.isContextual("await") && this.canAwait) { expr = this.parseAwait(forInit); sawUnary = true; } else if (this.type.prefix) { var node = this.startNode(), update = this.type === types$1.incDec; node.operator = this.value; node.prefix = true; this.next(); node.argument = this.parseMaybeUnary(null, true, update, forInit); this.checkExpressionErrors(refDestructuringErrors, true); if (update) { this.checkLValSimple(node.argument); } else if (this.strict && node.operator === "delete" && isLocalVariableAccess(node.argument)) { this.raiseRecoverable(node.start, "Deleting local variable in strict mode"); } else if (node.operator === "delete" && isPrivateFieldAccess(node.argument)) { this.raiseRecoverable(node.start, "Private fields can not be deleted"); } else { sawUnary = true; } expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression"); } else if (!sawUnary && this.type === types$1.privateId) { if ((forInit || this.privateNameStack.length === 0) && this.options.checkPrivateFields) { this.unexpected(); } expr = this.parsePrivateIdent(); // only could be private fields in 'in', such as #x in obj if (this.type !== types$1._in) { this.unexpected(); } } else { expr = this.parseExprSubscripts(refDestructuringErrors, forInit); if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } while (this.type.postfix && !this.canInsertSemicolon()) { var node$1 = this.startNodeAt(startPos, startLoc); node$1.operator = this.value; node$1.prefix = false; node$1.argument = expr; this.checkLValSimple(expr); this.next(); expr = this.finishNode(node$1, "UpdateExpression"); } } if (!incDec && this.eat(types$1.starstar)) { if (sawUnary) { this.unexpected(this.lastTokStart); } else { return this.buildBinary(startPos, startLoc, expr, this.parseMaybeUnary(null, false, false, forInit), "**", false) } } else { return expr } }; function isLocalVariableAccess(node) { return ( node.type === "Identifier" || node.type === "ParenthesizedExpression" && isLocalVariableAccess(node.expression) ) } function isPrivateFieldAccess(node) { return ( node.type === "MemberExpression" && node.property.type === "PrivateIdentifier" || node.type === "ChainExpression" && isPrivateFieldAccess(node.expression) || node.type === "ParenthesizedExpression" && isPrivateFieldAccess(node.expression) ) } // Parse call, dot, and `[]`-subscript expressions. pp$5.parseExprSubscripts = function(refDestructuringErrors, forInit) { var startPos = this.start, startLoc = this.startLoc; var expr = this.parseExprAtom(refDestructuringErrors, forInit); if (expr.type === "ArrowFunctionExpression" && this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")") { return expr } var result = this.parseSubscripts(expr, startPos, startLoc, false, forInit); if (refDestructuringErrors && result.type === "MemberExpression") { if (refDestructuringErrors.parenthesizedAssign >= result.start) { refDestructuringErrors.parenthesizedAssign = -1; } if (refDestructuringErrors.parenthesizedBind >= result.start) { refDestructuringErrors.parenthesizedBind = -1; } if (refDestructuringErrors.trailingComma >= result.start) { refDestructuringErrors.trailingComma = -1; } } return result }; pp$5.parseSubscripts = function(base, startPos, startLoc, noCalls, forInit) { var maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" && this.lastTokEnd === base.end && !this.canInsertSemicolon() && base.end - base.start === 5 && this.potentialArrowAt === base.start; var optionalChained = false; while (true) { var element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit); if (element.optional) { optionalChained = true; } if (element === base || element.type === "ArrowFunctionExpression") { if (optionalChained) { var chainNode = this.startNodeAt(startPos, startLoc); chainNode.expression = element; element = this.finishNode(chainNode, "ChainExpression"); } return element } base = element; } }; pp$5.shouldParseAsyncArrow = function() { return !this.canInsertSemicolon() && this.eat(types$1.arrow) }; pp$5.parseSubscriptAsyncArrow = function(startPos, startLoc, exprList, forInit) { return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList, true, forInit) }; pp$5.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit) { var optionalSupported = this.options.ecmaVersion >= 11; var optional = optionalSupported && this.eat(types$1.questionDot); if (noCalls && optional) { this.raise(this.lastTokStart, "Optional chaining cannot appear in the callee of new expressions"); } var computed = this.eat(types$1.bracketL); if (computed || (optional && this.type !== types$1.parenL && this.type !== types$1.backQuote) || this.eat(types$1.dot)) { var node = this.startNodeAt(startPos, startLoc); node.object = base; if (computed) { node.property = this.parseExpression(); this.expect(types$1.bracketR); } else if (this.type === types$1.privateId && base.type !== "Super") { node.property = this.parsePrivateIdent(); } else { node.property = this.parseIdent(this.options.allowReserved !== "never"); } node.computed = !!computed; if (optionalSupported) { node.optional = optional; } base = this.finishNode(node, "MemberExpression"); } else if (!noCalls && this.eat(types$1.parenL)) { var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos; this.yieldPos = 0; this.awaitPos = 0; this.awaitIdentPos = 0; var exprList = this.parseExprList(types$1.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors); if (maybeAsyncArrow && !optional && this.shouldParseAsyncArrow()) { this.checkPatternErrors(refDestructuringErrors, false); this.checkYieldAwaitInDefaultParams(); if (this.awaitIdentPos > 0) { this.raise(this.awaitIdentPos, "Cannot use 'await' as identifier inside an async function"); } this.yieldPos = oldYieldPos; this.awaitPos = oldAwaitPos; this.awaitIdentPos = oldAwaitIdentPos; return this.parseSubscriptAsyncArrow(startPos, startLoc, exprList, forInit) } this.checkExpressionErrors(refDestructuringErrors, true); this.yieldPos = oldYieldPos || this.yieldPos; this.awaitPos = oldAwaitPos || this.awaitPos; this.awaitIdentPos = oldAwaitIdentPos || this.awaitIdentPos; var node$1 = this.startNodeAt(startPos, startLoc); node$1.callee = base; node$1.arguments = exprList; if (optionalSupported) { node$1.optional = optional; } base = this.finishNode(node$1, "CallExpression"); } else if (this.type === types$1.backQuote) { if (optional || optionalChained) { this.raise(this.start, "Optional chaining cannot appear in the tag of tagged template expressions"); } var node$2 = this.startNodeAt(startPos, startLoc); node$2.tag = base; node$2.quasi = this.parseTemplate({isTagged: true}); base = this.finishNode(node$2, "TaggedTemplateExpression"); } return base }; // Parse an atomic expression — either a single token that is an // expression, an expression started by a keyword like `function` or // `new`, or an expression wrapped in punctuation like `()`, `[]`, // or `{}`. pp$5.parseExprAtom = function(refDestructuringErrors, forInit, forNew) { // If a division operator appears in an expression position, the // tokenizer got confused, and we force it to read a regexp instead. if (this.type === types$1.slash) { this.readRegexp(); } var node, canBeArrow = this.potentialArrowAt === this.start; switch (this.type) { case types$1._super: if (!this.allowSuper) { this.raise(this.start, "'super' keyword outside a method"); } node = this.startNode(); this.next(); if (this.type === types$1.parenL && !this.allowDirectSuper) { this.raise(node.start, "super() call outside constructor of a subclass"); } // The `super` keyword can appear at below: // SuperProperty: // super [ Expression ] // super . IdentifierName // SuperCall: // super ( Arguments ) if (this.type !== types$1.dot && this.type !== types$1.bracketL && this.type !== types$1.parenL) { this.unexpected(); } return this.finishNode(node, "Super") case types$1._this: node = this.startNode(); this.next(); return this.finishNode(node, "ThisExpression") case types$1.name: var startPos = this.start, startLoc = this.startLoc, containsEsc = this.containsEsc; var id = this.parseIdent(false); if (this.options.ecmaVersion >= 8 && !containsEsc && id.name === "async" && !this.canInsertSemicolon() && this.eat(types$1._function)) { this.overrideContext(types.f_expr); return this.parseFunction(this.startNodeAt(startPos, startLoc), 0, false, true, forInit) } if (canBeArrow && !this.canInsertSemicolon()) { if (this.eat(types$1.arrow)) { return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], false, forInit) } if (this.options.ecmaVersion >= 8 && id.name === "async" && this.type === types$1.name && !containsEsc && (!this.potentialArrowInForAwait || this.value !== "of" || this.containsEsc)) { id = this.parseIdent(false); if (this.canInsertSemicolon() || !this.eat(types$1.arrow)) { this.unexpected(); } return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], true, forInit) } } return id case types$1.regexp: var value = this.value; node = this.parseLiteral(value.value); node.regex = {pattern: value.pattern, flags: value.flags}; return node case types$1.num: case types$1.string: return this.parseLiteral(this.value) case types$1._null: case types$1._true: case types$1._false: node = this.startNode(); node.value = this.type === types$1._null ? null : this.type === types$1._true; node.raw = this.type.keyword; this.next(); return this.finishNode(node, "Literal") case types$1.parenL: var start = this.start, expr = this.parseParenAndDistinguishExpression(canBeArrow, forInit); if (refDestructuringErrors) { if (refDestructuringErrors.parenthesizedAssign < 0 && !this.isSimpleAssignTarget(expr)) { refDestructuringErrors.parenthesizedAssign = start; } if (refDestructuringErrors.parenthesizedBind < 0) { refDestructuringErrors.parenthesizedBind = start; } } return expr case types$1.bracketL: node = this.startNode(); this.next(); node.elements = this.parseExprList(types$1.bracketR, true, true, refDestructuringErrors); return this.finishNode(node, "ArrayExpression") case types$1.braceL: this.overrideContext(types.b_expr); return this.parseObj(false, refDestructuringErrors) case types$1._function: node = this.startNode(); this.next(); return this.parseFunction(node, 0) case types$1._class: return this.parseClass(this.startNode(), false) case types$1._new: return this.parseNew() case types$1.backQuote: return this.parseTemplate() case types$1._import: if (this.options.ecmaVersion >= 11) { return this.parseExprImport(forNew) } else { return this.unexpected() } default: return this.parseExprAtomDefault() } }; pp$5.parseExprAtomDefault = function() { this.unexpected(); }; pp$5.parseExprImport = function(forNew) { var node = this.startNode(); // Consume `import` as an identifier for `import.meta`. // Because `this.parseIdent(true)` doesn't check escape sequences, it needs the check of `this.containsEsc`. if (this.containsEsc) { this.raiseRecoverable(this.start, "Escape sequence in keyword import"); } this.next(); if (this.type === types$1.parenL && !forNew) { return this.parseDynamicImport(node) } else if (this.type === types$1.dot) { var meta = this.startNodeAt(node.start, node.loc && node.loc.start); meta.name = "import"; node.meta = this.finishNode(meta, "Identifier"); return this.parseImportMeta(node) } else { this.unexpected(); } }; pp$5.parseDynamicImport = function(node) { this.next(); // skip `(` // Parse node.source. node.source = this.parseMaybeAssign(); if (this.options.ecmaVersion >= 16) { if (!this.eat(types$1.parenR)) { this.expect(types$1.comma); if (!this.afterTrailingComma(types$1.parenR)) { node.options = this.parseMaybeAssign(); if (!this.eat(types$1.parenR)) { this.expect(types$1.comma); if (!this.afterTrailingComma(types$1.parenR)) { this.unexpected(); } } } else { node.options = null; } } else { node.options = null; } } else { // Verify ending. if (!this.eat(types$1.parenR)) { var errorPos = this.start; if (this.eat(types$1.comma) && this.eat(types$1.parenR)) { this.raiseRecoverable(errorPos, "Trailing comma is not allowed in import()"); } else { this.unexpected(errorPos); } } } return this.finishNode(node, "ImportExpression") }; pp$5.parseImportMeta = function(node) { this.next(); // skip `.` var containsEsc = this.containsEsc; node.property = this.parseIdent(true); if (node.property.name !== "meta") { this.raiseRecoverable(node.property.start, "The only valid meta property for import is 'import.meta'"); } if (containsEsc) { this.raiseRecoverable(node.start, "'import.meta' must not contain escaped characters"); } if (this.options.sourceType !== "module" && !this.options.allowImportExportEverywhere) { this.raiseRecoverable(node.start, "Cannot use 'import.meta' outside a module"); } return this.finishNode(node, "MetaProperty") }; pp$5.parseLiteral = function(value) { var node = this.startNode(); node.value = value; node.raw = this.input.slice(this.start, this.end); if (node.raw.charCodeAt(node.raw.length - 1) === 110) { node.bigint = node.value != null ? node.value.toString() : node.raw.slice(0, -1).replace(/_/g, ""); } this.next(); return this.finishNode(node, "Literal") }; pp$5.parseParenExpression = function() { this.expect(types$1.parenL); var val = this.parseExpression(); this.expect(types$1.parenR); return val }; pp$5.shouldParseArrow = function(exprList) { return !this.canInsertSemicolon() }; pp$5.parseParenAndDistinguishExpression = function(canBeArrow, forInit) { var startPos = this.start, startLoc = this.startLoc, val, allowTrailingComma = this.options.ecmaVersion >= 8; if (this.options.ecmaVersion >= 6) { this.next(); var innerStartPos = this.start, innerStartLoc = this.startLoc; var exprList = [], first = true, lastIsComma = false; var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, spreadStart; this.yieldPos = 0; this.awaitPos = 0; // Do not save awaitIdentPos to allow checking awaits nested in parameters while (this.type !== types$1.parenR) { first ? first = false : this.expect(types$1.comma); if (allowTrailingComma && this.afterTrailingComma(types$1.parenR, true)) { lastIsComma = true; break } else if (this.type === types$1.ellipsis) { spreadStart = this.start; exprList.push(this.parseParenItem(this.parseRestBinding())); if (this.type === types$1.comma) { this.raiseRecoverable( this.start, "Comma is not permitted after the rest element" ); } break } else { exprList.push(this.parseMaybeAssign(false, refDestructuringErrors, this.parseParenItem)); } } var innerEndPos = this.lastTokEnd, innerEndLoc = this.lastTokEndLoc; this.expect(types$1.parenR); if (canBeArrow && this.shouldParseArrow(exprList) && this.eat(types$1.arrow)) { this.checkPatternErrors(refDestructuringErrors, false); this.checkYieldAwaitInDefaultParams(); this.yieldPos = oldYieldPos; this.awaitPos = oldAwaitPos; return this.parseParenArrowList(startPos, startLoc, exprList, forInit) } if (!exprList.length || lastIsComma) { this.unexpected(this.lastTokStart); } if (spreadStart) { this.unexpected(spreadStart); } this.checkExpressionErrors(refDestructuringErrors, true); this.yieldPos = oldYieldPos || this.yieldPos; this.awaitPos = oldAwaitPos || this.awaitPos; if (exprList.length > 1) { val = this.startNodeAt(innerStartPos, innerStartLoc); val.expressions = exprList; this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc); } else { val = exprList[0]; } } else { val = this.parseParenExpression(); } if (this.options.preserveParens) { var par = this.startNodeAt(startPos, startLoc); par.expression = val; return this.finishNode(par, "ParenthesizedExpression") } else { return val } }; pp$5.parseParenItem = function(item) { return item }; pp$5.parseParenArrowList = function(startPos, startLoc, exprList, forInit) { return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList, false, forInit) }; // New's precedence is slightly tricky. It must allow its argument to // be a `[]` or dot subscript expression, but not a call — at least, // not without wrapping it in parentheses. Thus, it uses the noCalls // argument to parseSubscripts to prevent it from consuming the // argument list. var empty = []; pp$5.parseNew = function() { if (this.containsEsc) { this.raiseRecoverable(this.start, "Escape sequence in keyword new"); } var node = this.startNode(); this.next(); if (this.options.ecmaVersion >= 6 && this.type === types$1.dot) { var meta = this.startNodeAt(node.start, node.loc && node.loc.start); meta.name = "new"; node.meta = this.finishNode(meta, "Identifier"); this.next(); var containsEsc = this.containsEsc; node.property = this.parseIdent(true); if (node.property.name !== "target") { this.raiseRecoverable(node.property.start, "The only valid meta property for new is 'new.target'"); } if (containsEsc) { this.raiseRecoverable(node.start, "'new.target' must not contain escaped characters"); } if (!this.allowNewDotTarget) { this.raiseRecoverable(node.start, "'new.target' can only be used in functions and class static block"); } return this.finishNode(node, "MetaProperty") } var startPos = this.start, startLoc = this.startLoc; node.callee = this.parseSubscripts(this.parseExprAtom(null, false, true), startPos, startLoc, true, false); if (this.eat(types$1.parenL)) { node.arguments = this.parseExprList(types$1.parenR, this.options.ecmaVersion >= 8, false); } else { node.arguments = empty; } return this.finishNode(node, "NewExpression") }; // Parse template expression. pp$5.parseTemplateElement = function(ref) { var isTagged = ref.isTagged; var elem = this.startNode(); if (this.type === types$1.invalidTemplate) { if (!isTagged) { this.raiseRecoverable(this.start, "Bad escape sequence in untagged template literal"); } elem.value = { raw: this.value.replace(/\r\n?/g, "\n"), cooked: null }; } else { elem.value = { raw: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"), cooked: this.value }; } this.next(); elem.tail = this.type === types$1.backQuote; return this.finishNode(elem, "TemplateElement") }; pp$5.parseTemplate = function(ref) { if ( ref === void 0 ) ref = {}; var isTagged = ref.isTagged; if ( isTagged === void 0 ) isTagged = false; var node = this.startNode(); this.next(); node.expressions = []; var curElt = this.parseTemplateElement({isTagged: isTagged}); node.quasis = [curElt]; while (!curElt.tail) { if (this.type === types$1.eof) { this.raise(this.pos, "Unterminated template literal"); } this.expect(types$1.dollarBraceL); node.expressions.push(this.parseExpression()); this.expect(types$1.braceR); node.quasis.push(curElt = this.parseTemplateElement({isTagged: isTagged})); } this.next(); return this.finishNode(node, "TemplateLiteral") }; pp$5.isAsyncProp = function(prop) { return !prop.computed && prop.key.type === "Identifier" && prop.key.name === "async" && (this.type === types$1.name || this.type === types$1.num || this.type === types$1.string || this.type === types$1.bracketL || this.type.keyword || (this.options.ecmaVersion >= 9 && this.type === types$1.star)) && !lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) }; // Parse an object literal or binding pattern. pp$5.parseObj = function(isPattern, refDestructuringErrors) { var node = this.startNode(), first = true, propHash = {}; node.properties = []; this.next(); while (!this.eat(types$1.braceR)) { if (!first) { this.expect(types$1.comma); if (this.options.ecmaVersion >= 5 && this.afterTrailingComma(types$1.braceR)) { break } } else { first = false; } var prop = this.parseProperty(isPattern, refDestructuringErrors); if (!isPattern) { this.checkPropClash(prop, propHash, refDestructuringErrors); } node.properties.push(prop); } return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression") }; pp$5.parseProperty = function(isPattern, refDestructuringErrors) { var prop = this.startNode(), isGenerator, isAsync, startPos, startLoc; if (this.options.ecmaVersion >= 9 && this.eat(types$1.ellipsis)) { if (isPattern) { prop.argument = this.parseIdent(false); if (this.type === types$1.comma) { this.raiseRecoverable(this.start, "Comma is not permitted after the rest element"); } return this.finishNode(prop, "RestElement") } // Parse argument. prop.argument = this.parseMaybeAssign(false, refDestructuringErrors); // To disallow trailing comma via `this.toAssignable()`. if (this.type === types$1.comma && refDestructuringErrors && refDestructuringErrors.trailingComma < 0) { refDestructuringErrors.trailingComma = this.start; } // Finish return this.finishNode(prop, "SpreadElement") } if (this.options.ecmaVersion >= 6) { prop.method = false; prop.shorthand = false; if (isPattern || refDestructuringErrors) { startPos = this.start; startLoc = this.startLoc; } if (!isPattern) { isGenerator = this.eat(types$1.star); } } var containsEsc = this.containsEsc; this.parsePropertyName(prop); if (!isPattern && !containsEsc && this.options.ecmaVersion >= 8 && !isGenerator && this.isAsyncProp(prop)) { isAsync = true; isGenerator = this.options.ecmaVersion >= 9 && this.eat(types$1.star); this.parsePropertyName(prop); } else { isAsync = false; } this.parsePropertyValue(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc); return this.finishNode(prop, "Property") }; pp$5.parseGetterSetter = function(prop) { var kind = prop.key.name; this.parsePropertyName(prop); prop.value = this.parseMethod(false); prop.kind = kind; var paramCount = prop.kind === "get" ? 0 : 1; if (prop.value.params.length !== paramCount) { var start = prop.value.start; if (prop.kind === "get") { this.raiseRecoverable(start, "getter should have no params"); } else { this.raiseRecoverable(start, "setter should have exactly one param"); } } else { if (prop.kind === "set" && prop.value.params[0].type === "RestElement") { this.raiseRecoverable(prop.value.params[0].start, "Setter cannot use rest params"); } } }; pp$5.parsePropertyValue = function(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc) { if ((isGenerator || isAsync) && this.type === types$1.colon) { this.unexpected(); } if (this.eat(types$1.colon)) { prop.value = isPattern ? this.parseMaybeDefault(this.start, this.startLoc) : this.parseMaybeAssign(false, refDestructuringErrors); prop.kind = "init"; } else if (this.options.ecmaVersion >= 6 && this.type === types$1.parenL) { if (isPattern) { this.unexpected(); } prop.method = true; prop.value = this.parseMethod(isGenerator, isAsync); prop.kind = "init"; } else if (!isPattern && !containsEsc && this.options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" && (prop.key.name === "get" || prop.key.name === "set") && (this.type !== types$1.comma && this.type !== types$1.braceR && this.type !== types$1.eq)) { if (isGenerator || isAsync) { this.unexpected(); } this.parseGetterSetter(prop); } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") { if (isGenerator || isAsync) { this.unexpected(); } this.checkUnreserved(prop.key); if (prop.key.name === "await" && !this.awaitIdentPos) { this.awaitIdentPos = startPos; } if (isPattern) { prop.value = this.parseMaybeDefault(startPos, startLoc, this.copyNode(prop.key)); } else if (this.type === types$1.eq && refDestructuringErrors) { if (refDestructuringErrors.shorthandAssign < 0) { refDestructuringErrors.shorthandAssign = this.start; } prop.value = this.parseMaybeDefault(startPos, startLoc, this.copyNode(prop.key)); } else { prop.value = this.copyNode(prop.key); } prop.kind = "init"; prop.shorthand = true; } else { this.unexpected(); } }; pp$5.parsePropertyName = function(prop) { if (this.options.ecmaVersion >= 6) { if (this.eat(types$1.bracketL)) { prop.computed = true; prop.key = this.parseMaybeAssign(); this.expect(types$1.bracketR); return prop.key } else { prop.computed = false; } } return prop.key = this.type === types$1.num || this.type === types$1.string ? this.parseExprAtom() : this.parseIdent(this.options.allowReserved !== "never") }; // Initialize empty function node. pp$5.initFunction = function(node) { node.id = null; if (this.options.ecmaVersion >= 6) { node.generator = node.expression = false; } if (this.options.ecmaVersion >= 8) { node.async = false; } }; // Parse object or class method. pp$5.parseMethod = function(isGenerator, isAsync, allowDirectSuper) { var node = this.startNode(), oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos; this.initFunction(node); if (this.options.ecmaVersion >= 6) { node.generator = isGenerator; } if (this.options.ecmaVersion >= 8) { node.async = !!isAsync; } this.yieldPos = 0; this.awaitPos = 0; this.awaitIdentPos = 0; this.enterScope(functionFlags(isAsync, node.generator) | SCOPE_SUPER | (allowDirectSuper ? SCOPE_DIRECT_SUPER : 0)); this.expect(types$1.parenL); node.params = this.parseBindingList(types$1.parenR, false, this.options.ecmaVersion >= 8); this.checkYieldAwaitInDefaultParams(); this.parseFunctionBody(node, false, true, false); this.yieldPos = oldYieldPos; this.awaitPos = oldAwaitPos; this.awaitIdentPos = oldAwaitIdentPos; return this.finishNode(node, "FunctionExpression") }; // Parse arrow function expression with given parameters. pp$5.parseArrowExpression = function(node, params, isAsync, forInit) { var oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos; this.enterScope(functionFlags(isAsync, false) | SCOPE_ARROW); this.initFunction(node); if (this.options.ecmaVersion >= 8) { node.async = !!isAsync; } this.yieldPos = 0; this.awaitPos = 0; this.awaitIdentPos = 0; node.params = this.toAssignableList(params, true); this.parseFunctionBody(node, true, false, forInit); this.yieldPos = oldYieldPos; this.awaitPos = oldAwaitPos; this.awaitIdentPos = oldAwaitIdentPos; return this.finishNode(node, "ArrowFunctionExpression") }; // Parse function body and check parameters. pp$5.parseFunctionBody = function(node, isArrowFunction, isMethod, forInit) { var isExpression = isArrowFunction && this.type !== types$1.braceL; var oldStrict = this.strict, useStrict = false; if (isExpression) { node.body = this.parseMaybeAssign(forInit); node.expression = true; this.checkParams(node, false); } else { var nonSimple = this.options.ecmaVersion >= 7 && !this.isSimpleParamList(node.params); if (!oldStrict || nonSimple) { useStrict = this.strictDirective(this.end); // If this is a strict mode function, verify that argument names // are not repeated, and it does not try to bind the words `eval` // or `arguments`. if (useStrict && nonSimple) { this.raiseRecoverable(node.start, "Illegal 'use strict' directive in function with non-simple parameter list"); } } // Start a new scope with regard to labels and the `inFunction` // flag (restore them to their old value afterwards). var oldLabels = this.labels; this.labels = []; if (useStrict) { this.strict = true; } // Add the params to varDeclaredNames to ensure that an error is thrown // if a let/const declaration in the function clashes with one of the params. this.checkParams(node, !oldStrict && !useStrict && !isArrowFunction && !isMethod && this.isSimpleParamList(node.params)); // Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval' if (this.strict && node.id) { this.checkLValSimple(node.id, BIND_OUTSIDE); } node.body = this.parseBlock(false, undefined, useStrict && !oldStrict); node.expression = false; this.adaptDirectivePrologue(node.body.body); this.labels = oldLabels; } this.exitScope(); }; pp$5.isSimpleParamList = function(params) { for (var i = 0, list = params; i < list.length; i += 1) { var param = list[i]; if (param.type !== "Identifier") { return false } } return true }; // Checks function params for various disallowed patterns such as using "eval" // or "arguments" and duplicate parameters. pp$5.checkParams = function(node, allowDuplicates) { var nameHash = Object.create(null); for (var i = 0, list = node.params; i < list.length; i += 1) { var param = list[i]; this.checkLValInnerPattern(param, BIND_VAR, allowDuplicates ? null : nameHash); } }; // Parses a comma-separated list of expressions, and returns them as // an array. `close` is the token type that ends the list, and // `allowEmpty` can be turned on to allow subsequent commas with // nothing in between them to be parsed as `null` (which is needed // for array literals). pp$5.parseExprList = function(close, allowTrailingComma, allowEmpty, refDestructuringErrors) { var elts = [], first = true; while (!this.eat(close)) { if (!first) { this.expect(types$1.comma); if (allowTrailingComma && this.afterTrailingComma(close)) { break } } else { first = false; } var elt = (void 0); if (allowEmpty && this.type === types$1.comma) { elt = null; } else if (this.type === types$1.ellipsis) { elt = this.parseSpread(refDestructuringErrors); if (refDestructuringErrors && this.type === types$1.comma && refDestructuringErrors.trailingComma < 0) { refDestructuringErrors.trailingComma = this.start; } } else { elt = this.parseMaybeAssign(false, refDestructuringErrors); } elts.push(elt); } return elts }; pp$5.checkUnreserved = function(ref) { var start = ref.start; var end = ref.end; var name = ref.name; if (this.inGenerator && name === "yield") { this.raiseRecoverable(start, "Cannot use 'yield' as identifier inside a generator"); } if (this.inAsync && name === "await") { this.raiseRecoverable(start, "Cannot use 'await' as identifier inside an async function"); } if (!(this.currentThisScope().flags & SCOPE_VAR) && name === "arguments") { this.raiseRecoverable(start, "Cannot use 'arguments' in class field initializer"); } if (this.inClassStaticBlock && (name === "arguments" || name === "await")) { this.raise(start, ("Cannot use " + name + " in class static initialization block")); } if (this.keywords.test(name)) { this.raise(start, ("Unexpected keyword '" + name + "'")); } if (this.options.ecmaVersion < 6 && this.input.slice(start, end).indexOf("\\") !== -1) { return } var re = this.strict ? this.reservedWordsStrict : this.reservedWords; if (re.test(name)) { if (!this.inAsync && name === "await") { this.raiseRecoverable(start, "Cannot use keyword 'await' outside an async function"); } this.raiseRecoverable(start, ("The keyword '" + name + "' is reserved")); } }; // Parse the next token as an identifier. If `liberal` is true (used // when parsing properties), it will also convert keywords into // identifiers. pp$5.parseIdent = function(liberal) { var node = this.parseIdentNode(); this.next(!!liberal); this.finishNode(node, "Identifier"); if (!liberal) { this.checkUnreserved(node); if (node.name === "await" && !this.awaitIdentPos) { this.awaitIdentPos = node.start; } } return node }; pp$5.parseIdentNode = function() { var node = this.startNode(); if (this.type === types$1.name) { node.name = this.value; } else if (this.type.keyword) { node.name = this.type.keyword; // To fix https://github.com/acornjs/acorn/issues/575 // `class` and `function` keywords push new context into this.context. // But there is no chance to pop the context if the keyword is consumed as an identifier such as a property name. // If the previous token is a dot, this does not apply because the context-managing code already ignored the keyword if ((node.name === "class" || node.name === "function") && (this.lastTokEnd !== this.lastTokStart + 1 || this.input.charCodeAt(this.lastTokStart) !== 46)) { this.context.pop(); } this.type = types$1.name; } else { this.unexpected(); } return node }; pp$5.parsePrivateIdent = function() { var node = this.startNode(); if (this.type === types$1.privateId) { node.name = this.value; } else { this.unexpected(); } this.next(); this.finishNode(node, "PrivateIdentifier"); // For validating existence if (this.options.checkPrivateFields) { if (this.privateNameStack.length === 0) { this.raise(node.start, ("Private field '#" + (node.name) + "' must be declared in an enclosing class")); } else { this.privateNameStack[this.privateNameStack.length - 1].used.push(node); } } return node }; // Parses yield expression inside generator. pp$5.parseYield = function(forInit) { if (!this.yieldPos) { this.yieldPos = this.start; } var node = this.startNode(); this.next(); if (this.type === types$1.semi || this.canInsertSemicolon() || (this.type !== types$1.star && !this.type.startsExpr)) { node.delegate = false; node.argument = null; } else { node.delegate = this.eat(types$1.star); node.argument = this.parseMaybeAssign(forInit); } return this.finishNode(node, "YieldExpression") }; pp$5.parseAwait = function(forInit) { if (!this.awaitPos) { this.awaitPos = this.start; } var node = this.startNode(); this.next(); node.argument = this.parseMaybeUnary(null, true, false, forInit); return this.finishNode(node, "AwaitExpression") }; var pp$4 = Parser.prototype; // This function is used to raise exceptions on parse errors. It // takes an offset integer (into the current `input`) to indicate // the location of the error, attaches the position to the end // of the error message, and then raises a `SyntaxError` with that // message. pp$4.raise = function(pos, message) { var loc = getLineInfo(this.input, pos); message += " (" + loc.line + ":" + loc.column + ")"; if (this.sourceFile) { message += " in " + this.sourceFile; } var err = new SyntaxError(message); err.pos = pos; err.loc = loc; err.raisedAt = this.pos; throw err }; pp$4.raiseRecoverable = pp$4.raise; pp$4.curPosition = function() { if (this.options.locations) { return new Position(this.curLine, this.pos - this.lineStart) } }; var pp$3 = Parser.prototype; var Scope = function Scope(flags) { this.flags = flags; // A list of var-declared names in the current lexical scope this.var = []; // A list of lexically-declared names in the current lexical scope this.lexical = []; // A list of lexically-declared FunctionDeclaration names in the current lexical scope this.functions = []; }; // The functions in this module keep track of declared variables in the current scope in order to detect duplicate variable names. pp$3.enterScope = function(flags) { this.scopeStack.push(new Scope(flags)); }; pp$3.exitScope = function() { this.scopeStack.pop(); }; // The spec says: // > At the top level of a function, or script, function declarations are // > treated like var declarations rather than like lexical declarations. pp$3.treatFunctionsAsVarInScope = function(scope) { return (scope.flags & SCOPE_FUNCTION) || !this.inModule && (scope.flags & SCOPE_TOP) }; pp$3.declareName = function(name, bindingType, pos) { var redeclared = false; if (bindingType === BIND_LEXICAL) { var scope = this.currentScope(); redeclared = scope.lexical.indexOf(name) > -1 || scope.functions.indexOf(name) > -1 || scope.var.indexOf(name) > -1; scope.lexical.push(name); if (this.inModule && (scope.flags & SCOPE_TOP)) { delete this.undefinedExports[name]; } } else if (bindingType === BIND_SIMPLE_CATCH) { var scope$1 = this.currentScope(); scope$1.lexical.push(name); } else if (bindingType === BIND_FUNCTION) { var scope$2 = this.currentScope(); if (this.treatFunctionsAsVar) { redeclared = scope$2.lexical.indexOf(name) > -1; } else { redeclared = scope$2.lexical.indexOf(name) > -1 || scope$2.var.indexOf(name) > -1; } scope$2.functions.push(name); } else { for (var i = this.scopeStack.length - 1; i >= 0; --i) { var scope$3 = this.scopeStack[i]; if (scope$3.lexical.indexOf(name) > -1 && !((scope$3.flags & SCOPE_SIMPLE_CATCH) && scope$3.lexical[0] === name) || !this.treatFunctionsAsVarInScope(scope$3) && scope$3.functions.indexOf(name) > -1) { redeclared = true; break } scope$3.var.push(name); if (this.inModule && (scope$3.flags & SCOPE_TOP)) { delete this.undefinedExports[name]; } if (scope$3.flags & SCOPE_VAR) { break } } } if (redeclared) { this.raiseRecoverable(pos, ("Identifier '" + name + "' has already been declared")); } }; pp$3.checkLocalExport = function(id) { // scope.functions must be empty as Module code is always strict. if (this.scopeStack[0].lexical.indexOf(id.name) === -1 && this.scopeStack[0].var.indexOf(id.name) === -1) { this.undefinedExports[id.name] = id; } }; pp$3.currentScope = function() { return this.scopeStack[this.scopeStack.length - 1] }; pp$3.currentVarScope = function() { for (var i = this.scopeStack.length - 1;; i--) { var scope = this.scopeStack[i]; if (scope.flags & (SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK)) { return scope } } }; // Could be useful for `this`, `new.target`, `super()`, `super.property`, and `super[property]`. pp$3.currentThisScope = function() { for (var i = this.scopeStack.length - 1;; i--) { var scope = this.scopeStack[i]; if (scope.flags & (SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK) && !(scope.flags & SCOPE_ARROW)) { return scope } } }; var Node = function Node(parser, pos, loc) { this.type = ""; this.start = pos; this.end = 0; if (parser.options.locations) { this.loc = new SourceLocation(parser, loc); } if (parser.options.directSourceFile) { this.sourceFile = parser.options.directSourceFile; } if (parser.options.ranges) { this.range = [pos, 0]; } }; // Start an AST node, attaching a start offset. var pp$2 = Parser.prototype; pp$2.startNode = function() { return new Node(this, this.start, this.startLoc) }; pp$2.startNodeAt = function(pos, loc) { return new Node(this, pos, loc) }; // Finish an AST node, adding `type` and `end` properties. function finishNodeAt(node, type, pos, loc) { node.type = type; node.end = pos; if (this.options.locations) { node.loc.end = loc; } if (this.options.ranges) { node.range[1] = pos; } return node } pp$2.finishNode = function(node, type) { return finishNodeAt.call(this, node, type, this.lastTokEnd, this.lastTokEndLoc) }; // Finish node at given position pp$2.finishNodeAt = function(node, type, pos, loc) { return finishNodeAt.call(this, node, type, pos, loc) }; pp$2.copyNode = function(node) { var newNode = new Node(this, node.start, this.startLoc); for (var prop in node) { newNode[prop] = node[prop]; } return newNode }; // This file was generated by "bin/generate-unicode-script-values.js". Do not modify manually! var scriptValuesAddedInUnicode = "Gara Garay Gukh Gurung_Khema Hrkt Katakana_Or_Hiragana Kawi Kirat_Rai Krai Nag_Mundari Nagm Ol_Onal Onao Sunu Sunuwar Todhri Todr Tulu_Tigalari Tutg Unknown Zzzz"; // This file contains Unicode properties extracted from the ECMAScript specification. // The lists are extracted like so: // $$('#table-binary-unicode-properties > figure > table > tbody > tr > td:nth-child(1) code').map(el => el.innerText) // #table-binary-unicode-properties var ecma9BinaryProperties = "ASCII ASCII_Hex_Digit AHex Alphabetic Alpha Any Assigned Bidi_Control Bidi_C Bidi_Mirrored Bidi_M Case_Ignorable CI Cased Changes_When_Casefolded CWCF Changes_When_Casemapped CWCM Changes_When_Lowercased CWL Changes_When_NFKC_Casefolded CWKCF Changes_When_Titlecased CWT Changes_When_Uppercased CWU Dash Default_Ignorable_Code_Point DI Deprecated Dep Diacritic Dia Emoji Emoji_Component Emoji_Modifier Emoji_Modifier_Base Emoji_Presentation Extender Ext Grapheme_Base Gr_Base Grapheme_Extend Gr_Ext Hex_Digit Hex IDS_Binary_Operator IDSB IDS_Trinary_Operator IDST ID_Continue IDC ID_Start IDS Ideographic Ideo Join_Control Join_C Logical_Order_Exception LOE Lowercase Lower Math Noncharacter_Code_Point NChar Pattern_Syntax Pat_Syn Pattern_White_Space Pat_WS Quotation_Mark QMark Radical Regional_Indicator RI Sentence_Terminal STerm Soft_Dotted SD Terminal_Punctuation Term Unified_Ideograph UIdeo Uppercase Upper Variation_Selector VS White_Space space XID_Continue XIDC XID_Start XIDS"; var ecma10BinaryProperties = ecma9BinaryProperties + " Extended_Pictographic"; var ecma11BinaryProperties = ecma10BinaryProperties; var ecma12BinaryProperties = ecma11BinaryProperties + " EBase EComp EMod EPres ExtPict"; var ecma13BinaryProperties = ecma12BinaryProperties; var ecma14BinaryProperties = ecma13BinaryProperties; var unicodeBinaryProperties = { 9: ecma9BinaryProperties, 10: ecma10BinaryProperties, 11: ecma11BinaryProperties, 12: ecma12BinaryProperties, 13: ecma13BinaryProperties, 14: ecma14BinaryProperties }; // #table-binary-unicode-properties-of-strings var ecma14BinaryPropertiesOfStrings = "Basic_Emoji Emoji_Keycap_Sequence RGI_Emoji_Modifier_Sequence RGI_Emoji_Flag_Sequence RGI_Emoji_Tag_Sequence RGI_Emoji_ZWJ_Sequence RGI_Emoji"; var unicodeBinaryPropertiesOfStrings = { 9: "", 10: "", 11: "", 12: "", 13: "", 14: ecma14BinaryPropertiesOfStrings }; // #table-unicode-general-category-values var unicodeGeneralCategoryValues = "Cased_Letter LC Close_Punctuation Pe Connector_Punctuation Pc Control Cc cntrl Currency_Symbol Sc Dash_Punctuation Pd Decimal_Number Nd digit Enclosing_Mark Me Final_Punctuation Pf Format Cf Initial_Punctuation Pi Letter L Letter_Number Nl Line_Separator Zl Lowercase_Letter Ll Mark M Combining_Mark Math_Symbol Sm Modifier_Letter Lm Modifier_Symbol Sk Nonspacing_Mark Mn Number N Open_Punctuation Ps Other C Other_Letter Lo Other_Number No Other_Punctuation Po Other_Symbol So Paragraph_Separator Zp Private_Use Co Punctuation P punct Separator Z Space_Separator Zs Spacing_Mark Mc Surrogate Cs Symbol S Titlecase_Letter Lt Unassigned Cn Uppercase_Letter Lu"; // #table-unicode-script-values var ecma9ScriptValues = "Adlam Adlm Ahom Anatolian_Hieroglyphs Hluw Arabic Arab Armenian Armn Avestan Avst Balinese Bali Bamum Bamu Bassa_Vah Bass Batak Batk Bengali Beng Bhaiksuki Bhks Bopomofo Bopo Brahmi Brah Braille Brai Buginese Bugi Buhid Buhd Canadian_Aboriginal Cans Carian Cari Caucasian_Albanian Aghb Chakma Cakm Cham Cham Cherokee Cher Common Zyyy Coptic Copt Qaac Cuneiform Xsux Cypriot Cprt Cyrillic Cyrl Deseret Dsrt Devanagari Deva Duployan Dupl Egyptian_Hieroglyphs Egyp Elbasan Elba Ethiopic Ethi Georgian Geor Glagolitic Glag Gothic Goth Grantha Gran Greek Grek Gujarati Gujr Gurmukhi Guru Han Hani Hangul Hang Hanunoo Hano Hatran Hatr Hebrew Hebr Hiragana Hira Imperial_Aramaic Armi Inherited Zinh Qaai Inscriptional_Pahlavi Phli Inscriptional_Parthian Prti Javanese Java Kaithi Kthi Kannada Knda Katakana Kana Kayah_Li Kali Kharoshthi Khar Khmer Khmr Khojki Khoj Khudawadi Sind Lao Laoo Latin Latn Lepcha Lepc Limbu Limb Linear_A Lina Linear_B Linb Lisu Lisu Lycian Lyci Lydian Lydi Mahajani Mahj Malayalam Mlym Mandaic Mand Manichaean Mani Marchen Marc Masaram_Gondi Gonm Meetei_Mayek Mtei Mende_Kikakui Mend Meroitic_Cursive Merc Meroitic_Hieroglyphs Mero Miao Plrd Modi Mongolian Mong Mro Mroo Multani Mult Myanmar Mymr Nabataean Nbat New_Tai_Lue Talu Newa Newa Nko Nkoo Nushu Nshu Ogham Ogam Ol_Chiki Olck Old_Hungarian Hung Old_Italic Ital Old_North_Arabian Narb Old_Permic Perm Old_Persian Xpeo Old_South_Arabian Sarb Old_Turkic Orkh Oriya Orya Osage Osge Osmanya Osma Pahawh_Hmong Hmng Palmyrene Palm Pau_Cin_Hau Pauc Phags_Pa Phag Phoenician Phnx Psalter_Pahlavi Phlp Rejang Rjng Runic Runr Samaritan Samr Saurashtra Saur Sharada Shrd Shavian Shaw Siddham Sidd SignWriting Sgnw Sinhala Sinh Sora_Sompeng Sora Soyombo Soyo Sundanese Sund Syloti_Nagri Sylo Syriac Syrc Tagalog Tglg Tagbanwa Tagb Tai_Le Tale Tai_Tham Lana Tai_Viet Tavt Takri Takr Tamil Taml Tangut Tang Telugu Telu Thaana Thaa Thai Thai Tibetan Tibt Tifinagh Tfng Tirhuta Tirh Ugaritic Ugar Vai Vaii Warang_Citi Wara Yi Yiii Zanabazar_Square Zanb"; var ecma10ScriptValues = ecma9ScriptValues + " Dogra Dogr Gunjala_Gondi Gong Hanifi_Rohingya Rohg Makasar Maka Medefaidrin Medf Old_Sogdian Sogo Sogdian Sogd"; var ecma11ScriptValues = ecma10ScriptValues + " Elymaic Elym Nandinagari Nand Nyiakeng_Puachue_Hmong Hmnp Wancho Wcho"; var ecma12ScriptValues = ecma11ScriptValues + " Chorasmian Chrs Diak Dives_Akuru Khitan_Small_Script Kits Yezi Yezidi"; var ecma13ScriptValues = ecma12ScriptValues + " Cypro_Minoan Cpmn Old_Uyghur Ougr Tangsa Tnsa Toto Vithkuqi Vith"; var ecma14ScriptValues = ecma13ScriptValues + " " + scriptValuesAddedInUnicode; var unicodeScriptValues = { 9: ecma9ScriptValues, 10: ecma10ScriptValues, 11: ecma11ScriptValues, 12: ecma12ScriptValues, 13: ecma13ScriptValues, 14: ecma14ScriptValues }; var data$1 = {}; function buildUnicodeData(ecmaVersion) { var d = data$1[ecmaVersion] = { binary: wordsRegexp(unicodeBinaryProperties[ecmaVersion] + " " + unicodeGeneralCategoryValues), binaryOfStrings: wordsRegexp(unicodeBinaryPropertiesOfStrings[ecmaVersion]), nonBinary: { General_Category: wordsRegexp(unicodeGeneralCategoryValues), Script: wordsRegexp(unicodeScriptValues[ecmaVersion]) } }; d.nonBinary.Script_Extensions = d.nonBinary.Script; d.nonBinary.gc = d.nonBinary.General_Category; d.nonBinary.sc = d.nonBinary.Script; d.nonBinary.scx = d.nonBinary.Script_Extensions; } for (var i = 0, list = [9, 10, 11, 12, 13, 14]; i < list.length; i += 1) { var ecmaVersion = list[i]; buildUnicodeData(ecmaVersion); } var pp$1 = Parser.prototype; // Track disjunction structure to determine whether a duplicate // capture group name is allowed because it is in a separate branch. var BranchID = function BranchID(parent, base) { // Parent disjunction branch this.parent = parent; // Identifies this set of sibling branches this.base = base || this; }; BranchID.prototype.separatedFrom = function separatedFrom (alt) { // A branch is separate from another branch if they or any of // their parents are siblings in a given disjunction for (var self = this; self; self = self.parent) { for (var other = alt; other; other = other.parent) { if (self.base === other.base && self !== other) { return true } } } return false }; BranchID.prototype.sibling = function sibling () { return new BranchID(this.parent, this.base) }; var RegExpValidationState = function RegExpValidationState(parser) { this.parser = parser; this.validFlags = "gim" + (parser.options.ecmaVersion >= 6 ? "uy" : "") + (parser.options.ecmaVersion >= 9 ? "s" : "") + (parser.options.ecmaVersion >= 13 ? "d" : "") + (parser.options.ecmaVersion >= 15 ? "v" : ""); this.unicodeProperties = data$1[parser.options.ecmaVersion >= 14 ? 14 : parser.options.ecmaVersion]; this.source = ""; this.flags = ""; this.start = 0; this.switchU = false; this.switchV = false; this.switchN = false; this.pos = 0; this.lastIntValue = 0; this.lastStringValue = ""; this.lastAssertionIsQuantifiable = false; this.numCapturingParens = 0; this.maxBackReference = 0; this.groupNames = Object.create(null); this.backReferenceNames = []; this.branchID = null; }; RegExpValidationState.prototype.reset = function reset (start, pattern, flags) { var unicodeSets = flags.indexOf("v") !== -1; var unicode = flags.indexOf("u") !== -1; this.start = start | 0; this.source = pattern + ""; this.flags = flags; if (unicodeSets && this.parser.options.ecmaVersion >= 15) { this.switchU = true; this.switchV = true; this.switchN = true; } else { this.switchU = unicode && this.parser.options.ecmaVersion >= 6; this.switchV = false; this.switchN = unicode && this.parser.options.ecmaVersion >= 9; } }; RegExpValidationState.prototype.raise = function raise (message) { this.parser.raiseRecoverable(this.start, ("Invalid regular expression: /" + (this.source) + "/: " + message)); }; // If u flag is given, this returns the code point at the index (it combines a surrogate pair). // Otherwise, this returns the code unit of the index (can be a part of a surrogate pair). RegExpValidationState.prototype.at = function at (i, forceU) { if ( forceU === void 0 ) forceU = false; var s = this.source; var l = s.length; if (i >= l) { return -1 } var c = s.charCodeAt(i); if (!(forceU || this.switchU) || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l) { return c } var next = s.charCodeAt(i + 1); return next >= 0xDC00 && next <= 0xDFFF ? (c << 10) + next - 0x35FDC00 : c }; RegExpValidationState.prototype.nextIndex = function nextIndex (i, forceU) { if ( forceU === void 0 ) forceU = false; var s = this.source; var l = s.length; if (i >= l) { return l } var c = s.charCodeAt(i), next; if (!(forceU || this.switchU) || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l || (next = s.charCodeAt(i + 1)) < 0xDC00 || next > 0xDFFF) { return i + 1 } return i + 2 }; RegExpValidationState.prototype.current = function current (forceU) { if ( forceU === void 0 ) forceU = false; return this.at(this.pos, forceU) }; RegExpValidationState.prototype.lookahead = function lookahead (forceU) { if ( forceU === void 0 ) forceU = false; return this.at(this.nextIndex(this.pos, forceU), forceU) }; RegExpValidationState.prototype.advance = function advance (forceU) { if ( forceU === void 0 ) forceU = false; this.pos = this.nextIndex(this.pos, forceU); }; RegExpValidationState.prototype.eat = function eat (ch, forceU) { if ( forceU === void 0 ) forceU = false; if (this.current(forceU) === ch) { this.advance(forceU); return true } return false }; RegExpValidationState.prototype.eatChars = function eatChars (chs, forceU) { if ( forceU === void 0 ) forceU = false; var pos = this.pos; for (var i = 0, list = chs; i < list.length; i += 1) { var ch = list[i]; var current = this.at(pos, forceU); if (current === -1 || current !== ch) { return false } pos = this.nextIndex(pos, forceU); } this.pos = pos; return true }; /** * Validate the flags part of a given RegExpLiteral. * * @param {RegExpValidationState} state The state to validate RegExp. * @returns {void} */ pp$1.validateRegExpFlags = function(state) { var validFlags = state.validFlags; var flags = state.flags; var u = false; var v = false; for (var i = 0; i < flags.length; i++) { var flag = flags.charAt(i); if (validFlags.indexOf(flag) === -1) { this.raise(state.start, "Invalid regular expression flag"); } if (flags.indexOf(flag, i + 1) > -1) { this.raise(state.start, "Duplicate regular expression flag"); } if (flag === "u") { u = true; } if (flag === "v") { v = true; } } if (this.options.ecmaVersion >= 15 && u && v) { this.raise(state.start, "Invalid regular expression flag"); } }; function hasProp(obj) { for (var _ in obj) { return true } return false } /** * Validate the pattern part of a given RegExpLiteral. * * @param {RegExpValidationState} state The state to validate RegExp. * @returns {void} */ pp$1.validateRegExpPattern = function(state) { this.regexp_pattern(state); // The goal symbol for the parse is |Pattern[~U, ~N]|. If the result of // parsing contains a |GroupName|, reparse with the goal symbol // |Pattern[~U, +N]| and use this result instead. Throw a *SyntaxError* // exception if _P_ did not conform to the grammar, if any elements of _P_ // were not matched by the parse, or if any Early Error conditions exist. if (!state.switchN && this.options.ecmaVersion >= 9 && hasProp(state.groupNames)) { state.switchN = true; this.regexp_pattern(state); } }; // https://www.ecma-international.org/ecma-262/8.0/#prod-Pattern pp$1.regexp_pattern = function(state) { state.pos = 0; state.lastIntValue = 0; state.lastStringValue = ""; state.lastAssertionIsQuantifiable = false; state.numCapturingParens = 0; state.maxBackReference = 0; state.groupNames = Object.create(null); state.backReferenceNames.length = 0; state.branchID = null; this.regexp_disjunction(state); if (state.pos !== state.source.length) { // Make the same messages as V8. if (state.eat(0x29 /* ) */)) { state.raise("Unmatched ')'"); } if (state.eat(0x5D /* ] */) || state.eat(0x7D /* } */)) { state.raise("Lone quantifier brackets"); } } if (state.maxBackReference > state.numCapturingParens) { state.raise("Invalid escape"); } for (var i = 0, list = state.backReferenceNames; i < list.length; i += 1) { var name = list[i]; if (!state.groupNames[name]) { state.raise("Invalid named capture referenced"); } } }; // https://www.ecma-international.org/ecma-262/8.0/#prod-Disjunction pp$1.regexp_disjunction = function(state) { var trackDisjunction = this.options.ecmaVersion >= 16; if (trackDisjunction) { state.branchID = new BranchID(state.branchID, null); } this.regexp_alternative(state); while (state.eat(0x7C /* | */)) { if (trackDisjunction) { state.branchID = state.branchID.sibling(); } this.regexp_alternative(state); } if (trackDisjunction) { state.branchID = state.branchID.parent; } // Make the same message as V8. if (this.regexp_eatQuantifier(state, true)) { state.raise("Nothing to repeat"); } if (state.eat(0x7B /* { */)) { state.raise("Lone quantifier brackets"); } }; // https://www.ecma-international.org/ecma-262/8.0/#prod-Alternative pp$1.regexp_alternative = function(state) { while (state.pos < state.source.length && this.regexp_eatTerm(state)) {} }; // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Term pp$1.regexp_eatTerm = function(state) { if (this.regexp_eatAssertion(state)) { // Handle `QuantifiableAssertion Quantifier` alternative. // `state.lastAssertionIsQuantifiable` is true if the last eaten Assertion // is a QuantifiableAssertion. if (state.lastAssertionIsQuantifiable && this.regexp_eatQuantifier(state)) { // Make the same message as V8. if (state.switchU) { state.raise("Invalid quantifier"); } } return true } if (state.switchU ? this.regexp_eatAtom(state) : this.regexp_eatExtendedAtom(state)) { this.regexp_eatQuantifier(state); return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Assertion pp$1.regexp_eatAssertion = function(state) { var start = state.pos; state.lastAssertionIsQuantifiable = false; // ^, $ if (state.eat(0x5E /* ^ */) || state.eat(0x24 /* $ */)) { return true } // \b \B if (state.eat(0x5C /* \ */)) { if (state.eat(0x42 /* B */) || state.eat(0x62 /* b */)) { return true } state.pos = start; } // Lookahead / Lookbehind if (state.eat(0x28 /* ( */) && state.eat(0x3F /* ? */)) { var lookbehind = false; if (this.options.ecmaVersion >= 9) { lookbehind = state.eat(0x3C /* < */); } if (state.eat(0x3D /* = */) || state.eat(0x21 /* ! */)) { this.regexp_disjunction(state); if (!state.eat(0x29 /* ) */)) { state.raise("Unterminated group"); } state.lastAssertionIsQuantifiable = !lookbehind; return true } } state.pos = start; return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-Quantifier pp$1.regexp_eatQuantifier = function(state, noError) { if ( noError === void 0 ) noError = false; if (this.regexp_eatQuantifierPrefix(state, noError)) { state.eat(0x3F /* ? */); return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-QuantifierPrefix pp$1.regexp_eatQuantifierPrefix = function(state, noError) { return ( state.eat(0x2A /* * */) || state.eat(0x2B /* + */) || state.eat(0x3F /* ? */) || this.regexp_eatBracedQuantifier(state, noError) ) }; pp$1.regexp_eatBracedQuantifier = function(state, noError) { var start = state.pos; if (state.eat(0x7B /* { */)) { var min = 0, max = -1; if (this.regexp_eatDecimalDigits(state)) { min = state.lastIntValue; if (state.eat(0x2C /* , */) && this.regexp_eatDecimalDigits(state)) { max = state.lastIntValue; } if (state.eat(0x7D /* } */)) { // SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-term if (max !== -1 && max < min && !noError) { state.raise("numbers out of order in {} quantifier"); } return true } } if (state.switchU && !noError) { state.raise("Incomplete quantifier"); } state.pos = start; } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-Atom pp$1.regexp_eatAtom = function(state) { return ( this.regexp_eatPatternCharacters(state) || state.eat(0x2E /* . */) || this.regexp_eatReverseSolidusAtomEscape(state) || this.regexp_eatCharacterClass(state) || this.regexp_eatUncapturingGroup(state) || this.regexp_eatCapturingGroup(state) ) }; pp$1.regexp_eatReverseSolidusAtomEscape = function(state) { var start = state.pos; if (state.eat(0x5C /* \ */)) { if (this.regexp_eatAtomEscape(state)) { return true } state.pos = start; } return false }; pp$1.regexp_eatUncapturingGroup = function(state) { var start = state.pos; if (state.eat(0x28 /* ( */)) { if (state.eat(0x3F /* ? */)) { if (this.options.ecmaVersion >= 16) { var addModifiers = this.regexp_eatModifiers(state); var hasHyphen = state.eat(0x2D /* - */); if (addModifiers || hasHyphen) { for (var i = 0; i < addModifiers.length; i++) { var modifier = addModifiers.charAt(i); if (addModifiers.indexOf(modifier, i + 1) > -1) { state.raise("Duplicate regular expression modifiers"); } } if (hasHyphen) { var removeModifiers = this.regexp_eatModifiers(state); if (!addModifiers && !removeModifiers && state.current() === 0x3A /* : */) { state.raise("Invalid regular expression modifiers"); } for (var i$1 = 0; i$1 < removeModifiers.length; i$1++) { var modifier$1 = removeModifiers.charAt(i$1); if ( removeModifiers.indexOf(modifier$1, i$1 + 1) > -1 || addModifiers.indexOf(modifier$1) > -1 ) { state.raise("Duplicate regular expression modifiers"); } } } } } if (state.eat(0x3A /* : */)) { this.regexp_disjunction(state); if (state.eat(0x29 /* ) */)) { return true } state.raise("Unterminated group"); } } state.pos = start; } return false }; pp$1.regexp_eatCapturingGroup = function(state) { if (state.eat(0x28 /* ( */)) { if (this.options.ecmaVersion >= 9) { this.regexp_groupSpecifier(state); } else if (state.current() === 0x3F /* ? */) { state.raise("Invalid group"); } this.regexp_disjunction(state); if (state.eat(0x29 /* ) */)) { state.numCapturingParens += 1; return true } state.raise("Unterminated group"); } return false }; // RegularExpressionModifiers :: // [empty] // RegularExpressionModifiers RegularExpressionModifier pp$1.regexp_eatModifiers = function(state) { var modifiers = ""; var ch = 0; while ((ch = state.current()) !== -1 && isRegularExpressionModifier(ch)) { modifiers += codePointToString(ch); state.advance(); } return modifiers }; // RegularExpressionModifier :: one of // `i` `m` `s` function isRegularExpressionModifier(ch) { return ch === 0x69 /* i */ || ch === 0x6d /* m */ || ch === 0x73 /* s */ } // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedAtom pp$1.regexp_eatExtendedAtom = function(state) { return ( state.eat(0x2E /* . */) || this.regexp_eatReverseSolidusAtomEscape(state) || this.regexp_eatCharacterClass(state) || this.regexp_eatUncapturingGroup(state) || this.regexp_eatCapturingGroup(state) || this.regexp_eatInvalidBracedQuantifier(state) || this.regexp_eatExtendedPatternCharacter(state) ) }; // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-InvalidBracedQuantifier pp$1.regexp_eatInvalidBracedQuantifier = function(state) { if (this.regexp_eatBracedQuantifier(state, true)) { state.raise("Nothing to repeat"); } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-SyntaxCharacter pp$1.regexp_eatSyntaxCharacter = function(state) { var ch = state.current(); if (isSyntaxCharacter(ch)) { state.lastIntValue = ch; state.advance(); return true } return false }; function isSyntaxCharacter(ch) { return ( ch === 0x24 /* $ */ || ch >= 0x28 /* ( */ && ch <= 0x2B /* + */ || ch === 0x2E /* . */ || ch === 0x3F /* ? */ || ch >= 0x5B /* [ */ && ch <= 0x5E /* ^ */ || ch >= 0x7B /* { */ && ch <= 0x7D /* } */ ) } // https://www.ecma-international.org/ecma-262/8.0/#prod-PatternCharacter // But eat eager. pp$1.regexp_eatPatternCharacters = function(state) { var start = state.pos; var ch = 0; while ((ch = state.current()) !== -1 && !isSyntaxCharacter(ch)) { state.advance(); } return state.pos !== start }; // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedPatternCharacter pp$1.regexp_eatExtendedPatternCharacter = function(state) { var ch = state.current(); if ( ch !== -1 && ch !== 0x24 /* $ */ && !(ch >= 0x28 /* ( */ && ch <= 0x2B /* + */) && ch !== 0x2E /* . */ && ch !== 0x3F /* ? */ && ch !== 0x5B /* [ */ && ch !== 0x5E /* ^ */ && ch !== 0x7C /* | */ ) { state.advance(); return true } return false }; // GroupSpecifier :: // [empty] // `?` GroupName pp$1.regexp_groupSpecifier = function(state) { if (state.eat(0x3F /* ? */)) { if (!this.regexp_eatGroupName(state)) { state.raise("Invalid group"); } var trackDisjunction = this.options.ecmaVersion >= 16; var known = state.groupNames[state.lastStringValue]; if (known) { if (trackDisjunction) { for (var i = 0, list = known; i < list.length; i += 1) { var altID = list[i]; if (!altID.separatedFrom(state.branchID)) { state.raise("Duplicate capture group name"); } } } else { state.raise("Duplicate capture group name"); } } if (trackDisjunction) { (known || (state.groupNames[state.lastStringValue] = [])).push(state.branchID); } else { state.groupNames[state.lastStringValue] = true; } } }; // GroupName :: // `<` RegExpIdentifierName `>` // Note: this updates `state.lastStringValue` property with the eaten name. pp$1.regexp_eatGroupName = function(state) { state.lastStringValue = ""; if (state.eat(0x3C /* < */)) { if (this.regexp_eatRegExpIdentifierName(state) && state.eat(0x3E /* > */)) { return true } state.raise("Invalid capture group name"); } return false }; // RegExpIdentifierName :: // RegExpIdentifierStart // RegExpIdentifierName RegExpIdentifierPart // Note: this updates `state.lastStringValue` property with the eaten name. pp$1.regexp_eatRegExpIdentifierName = function(state) { state.lastStringValue = ""; if (this.regexp_eatRegExpIdentifierStart(state)) { state.lastStringValue += codePointToString(state.lastIntValue); while (this.regexp_eatRegExpIdentifierPart(state)) { state.lastStringValue += codePointToString(state.lastIntValue); } return true } return false }; // RegExpIdentifierStart :: // UnicodeIDStart // `$` // `_` // `\` RegExpUnicodeEscapeSequence[+U] pp$1.regexp_eatRegExpIdentifierStart = function(state) { var start = state.pos; var forceU = this.options.ecmaVersion >= 11; var ch = state.current(forceU); state.advance(forceU); if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state, forceU)) { ch = state.lastIntValue; } if (isRegExpIdentifierStart(ch)) { state.lastIntValue = ch; return true } state.pos = start; return false }; function isRegExpIdentifierStart(ch) { return isIdentifierStart(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */ } // RegExpIdentifierPart :: // UnicodeIDContinue // `$` // `_` // `\` RegExpUnicodeEscapeSequence[+U] // // pp$1.regexp_eatRegExpIdentifierPart = function(state) { var start = state.pos; var forceU = this.options.ecmaVersion >= 11; var ch = state.current(forceU); state.advance(forceU); if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state, forceU)) { ch = state.lastIntValue; } if (isRegExpIdentifierPart(ch)) { state.lastIntValue = ch; return true } state.pos = start; return false }; function isRegExpIdentifierPart(ch) { return isIdentifierChar(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */ || ch === 0x200C /* */ || ch === 0x200D /* */ } // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-AtomEscape pp$1.regexp_eatAtomEscape = function(state) { if ( this.regexp_eatBackReference(state) || this.regexp_eatCharacterClassEscape(state) || this.regexp_eatCharacterEscape(state) || (state.switchN && this.regexp_eatKGroupName(state)) ) { return true } if (state.switchU) { // Make the same message as V8. if (state.current() === 0x63 /* c */) { state.raise("Invalid unicode escape"); } state.raise("Invalid escape"); } return false }; pp$1.regexp_eatBackReference = function(state) { var start = state.pos; if (this.regexp_eatDecimalEscape(state)) { var n = state.lastIntValue; if (state.switchU) { // For SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-atomescape if (n > state.maxBackReference) { state.maxBackReference = n; } return true } if (n <= state.numCapturingParens) { return true } state.pos = start; } return false }; pp$1.regexp_eatKGroupName = function(state) { if (state.eat(0x6B /* k */)) { if (this.regexp_eatGroupName(state)) { state.backReferenceNames.push(state.lastStringValue); return true } state.raise("Invalid named reference"); } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-CharacterEscape pp$1.regexp_eatCharacterEscape = function(state) { return ( this.regexp_eatControlEscape(state) || this.regexp_eatCControlLetter(state) || this.regexp_eatZero(state) || this.regexp_eatHexEscapeSequence(state) || this.regexp_eatRegExpUnicodeEscapeSequence(state, false) || (!state.switchU && this.regexp_eatLegacyOctalEscapeSequence(state)) || this.regexp_eatIdentityEscape(state) ) }; pp$1.regexp_eatCControlLetter = function(state) { var start = state.pos; if (state.eat(0x63 /* c */)) { if (this.regexp_eatControlLetter(state)) { return true } state.pos = start; } return false }; pp$1.regexp_eatZero = function(state) { if (state.current() === 0x30 /* 0 */ && !isDecimalDigit(state.lookahead())) { state.lastIntValue = 0; state.advance(); return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-ControlEscape pp$1.regexp_eatControlEscape = function(state) { var ch = state.current(); if (ch === 0x74 /* t */) { state.lastIntValue = 0x09; /* \t */ state.advance(); return true } if (ch === 0x6E /* n */) { state.lastIntValue = 0x0A; /* \n */ state.advance(); return true } if (ch === 0x76 /* v */) { state.lastIntValue = 0x0B; /* \v */ state.advance(); return true } if (ch === 0x66 /* f */) { state.lastIntValue = 0x0C; /* \f */ state.advance(); return true } if (ch === 0x72 /* r */) { state.lastIntValue = 0x0D; /* \r */ state.advance(); return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-ControlLetter pp$1.regexp_eatControlLetter = function(state) { var ch = state.current(); if (isControlLetter(ch)) { state.lastIntValue = ch % 0x20; state.advance(); return true } return false }; function isControlLetter(ch) { return ( (ch >= 0x41 /* A */ && ch <= 0x5A /* Z */) || (ch >= 0x61 /* a */ && ch <= 0x7A /* z */) ) } // https://www.ecma-international.org/ecma-262/8.0/#prod-RegExpUnicodeEscapeSequence pp$1.regexp_eatRegExpUnicodeEscapeSequence = function(state, forceU) { if ( forceU === void 0 ) forceU = false; var start = state.pos; var switchU = forceU || state.switchU; if (state.eat(0x75 /* u */)) { if (this.regexp_eatFixedHexDigits(state, 4)) { var lead = state.lastIntValue; if (switchU && lead >= 0xD800 && lead <= 0xDBFF) { var leadSurrogateEnd = state.pos; if (state.eat(0x5C /* \ */) && state.eat(0x75 /* u */) && this.regexp_eatFixedHexDigits(state, 4)) { var trail = state.lastIntValue; if (trail >= 0xDC00 && trail <= 0xDFFF) { state.lastIntValue = (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000; return true } } state.pos = leadSurrogateEnd; state.lastIntValue = lead; } return true } if ( switchU && state.eat(0x7B /* { */) && this.regexp_eatHexDigits(state) && state.eat(0x7D /* } */) && isValidUnicode(state.lastIntValue) ) { return true } if (switchU) { state.raise("Invalid unicode escape"); } state.pos = start; } return false }; function isValidUnicode(ch) { return ch >= 0 && ch <= 0x10FFFF } // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-IdentityEscape pp$1.regexp_eatIdentityEscape = function(state) { if (state.switchU) { if (this.regexp_eatSyntaxCharacter(state)) { return true } if (state.eat(0x2F /* / */)) { state.lastIntValue = 0x2F; /* / */ return true } return false } var ch = state.current(); if (ch !== 0x63 /* c */ && (!state.switchN || ch !== 0x6B /* k */)) { state.lastIntValue = ch; state.advance(); return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalEscape pp$1.regexp_eatDecimalEscape = function(state) { state.lastIntValue = 0; var ch = state.current(); if (ch >= 0x31 /* 1 */ && ch <= 0x39 /* 9 */) { do { state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */); state.advance(); } while ((ch = state.current()) >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */) return true } return false }; // Return values used by character set parsing methods, needed to // forbid negation of sets that can match strings. var CharSetNone = 0; // Nothing parsed var CharSetOk = 1; // Construct parsed, cannot contain strings var CharSetString = 2; // Construct parsed, can contain strings // https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClassEscape pp$1.regexp_eatCharacterClassEscape = function(state) { var ch = state.current(); if (isCharacterClassEscape(ch)) { state.lastIntValue = -1; state.advance(); return CharSetOk } var negate = false; if ( state.switchU && this.options.ecmaVersion >= 9 && ((negate = ch === 0x50 /* P */) || ch === 0x70 /* p */) ) { state.lastIntValue = -1; state.advance(); var result; if ( state.eat(0x7B /* { */) && (result = this.regexp_eatUnicodePropertyValueExpression(state)) && state.eat(0x7D /* } */) ) { if (negate && result === CharSetString) { state.raise("Invalid property name"); } return result } state.raise("Invalid property name"); } return CharSetNone }; function isCharacterClassEscape(ch) { return ( ch === 0x64 /* d */ || ch === 0x44 /* D */ || ch === 0x73 /* s */ || ch === 0x53 /* S */ || ch === 0x77 /* w */ || ch === 0x57 /* W */ ) } // UnicodePropertyValueExpression :: // UnicodePropertyName `=` UnicodePropertyValue // LoneUnicodePropertyNameOrValue pp$1.regexp_eatUnicodePropertyValueExpression = function(state) { var start = state.pos; // UnicodePropertyName `=` UnicodePropertyValue if (this.regexp_eatUnicodePropertyName(state) && state.eat(0x3D /* = */)) { var name = state.lastStringValue; if (this.regexp_eatUnicodePropertyValue(state)) { var value = state.lastStringValue; this.regexp_validateUnicodePropertyNameAndValue(state, name, value); return CharSetOk } } state.pos = start; // LoneUnicodePropertyNameOrValue if (this.regexp_eatLoneUnicodePropertyNameOrValue(state)) { var nameOrValue = state.lastStringValue; return this.regexp_validateUnicodePropertyNameOrValue(state, nameOrValue) } return CharSetNone }; pp$1.regexp_validateUnicodePropertyNameAndValue = function(state, name, value) { if (!hasOwn(state.unicodeProperties.nonBinary, name)) { state.raise("Invalid property name"); } if (!state.unicodeProperties.nonBinary[name].test(value)) { state.raise("Invalid property value"); } }; pp$1.regexp_validateUnicodePropertyNameOrValue = function(state, nameOrValue) { if (state.unicodeProperties.binary.test(nameOrValue)) { return CharSetOk } if (state.switchV && state.unicodeProperties.binaryOfStrings.test(nameOrValue)) { return CharSetString } state.raise("Invalid property name"); }; // UnicodePropertyName :: // UnicodePropertyNameCharacters pp$1.regexp_eatUnicodePropertyName = function(state) { var ch = 0; state.lastStringValue = ""; while (isUnicodePropertyNameCharacter(ch = state.current())) { state.lastStringValue += codePointToString(ch); state.advance(); } return state.lastStringValue !== "" }; function isUnicodePropertyNameCharacter(ch) { return isControlLetter(ch) || ch === 0x5F /* _ */ } // UnicodePropertyValue :: // UnicodePropertyValueCharacters pp$1.regexp_eatUnicodePropertyValue = function(state) { var ch = 0; state.lastStringValue = ""; while (isUnicodePropertyValueCharacter(ch = state.current())) { state.lastStringValue += codePointToString(ch); state.advance(); } return state.lastStringValue !== "" }; function isUnicodePropertyValueCharacter(ch) { return isUnicodePropertyNameCharacter(ch) || isDecimalDigit(ch) } // LoneUnicodePropertyNameOrValue :: // UnicodePropertyValueCharacters pp$1.regexp_eatLoneUnicodePropertyNameOrValue = function(state) { return this.regexp_eatUnicodePropertyValue(state) }; // https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClass pp$1.regexp_eatCharacterClass = function(state) { if (state.eat(0x5B /* [ */)) { var negate = state.eat(0x5E /* ^ */); var result = this.regexp_classContents(state); if (!state.eat(0x5D /* ] */)) { state.raise("Unterminated character class"); } if (negate && result === CharSetString) { state.raise("Negated character class may contain strings"); } return true } return false }; // https://tc39.es/ecma262/#prod-ClassContents // https://www.ecma-international.org/ecma-262/8.0/#prod-ClassRanges pp$1.regexp_classContents = function(state) { if (state.current() === 0x5D /* ] */) { return CharSetOk } if (state.switchV) { return this.regexp_classSetExpression(state) } this.regexp_nonEmptyClassRanges(state); return CharSetOk }; // https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRanges // https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRangesNoDash pp$1.regexp_nonEmptyClassRanges = function(state) { while (this.regexp_eatClassAtom(state)) { var left = state.lastIntValue; if (state.eat(0x2D /* - */) && this.regexp_eatClassAtom(state)) { var right = state.lastIntValue; if (state.switchU && (left === -1 || right === -1)) { state.raise("Invalid character class"); } if (left !== -1 && right !== -1 && left > right) { state.raise("Range out of order in character class"); } } } }; // https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtom // https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtomNoDash pp$1.regexp_eatClassAtom = function(state) { var start = state.pos; if (state.eat(0x5C /* \ */)) { if (this.regexp_eatClassEscape(state)) { return true } if (state.switchU) { // Make the same message as V8. var ch$1 = state.current(); if (ch$1 === 0x63 /* c */ || isOctalDigit(ch$1)) { state.raise("Invalid class escape"); } state.raise("Invalid escape"); } state.pos = start; } var ch = state.current(); if (ch !== 0x5D /* ] */) { state.lastIntValue = ch; state.advance(); return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassEscape pp$1.regexp_eatClassEscape = function(state) { var start = state.pos; if (state.eat(0x62 /* b */)) { state.lastIntValue = 0x08; /* */ return true } if (state.switchU && state.eat(0x2D /* - */)) { state.lastIntValue = 0x2D; /* - */ return true } if (!state.switchU && state.eat(0x63 /* c */)) { if (this.regexp_eatClassControlLetter(state)) { return true } state.pos = start; } return ( this.regexp_eatCharacterClassEscape(state) || this.regexp_eatCharacterEscape(state) ) }; // https://tc39.es/ecma262/#prod-ClassSetExpression // https://tc39.es/ecma262/#prod-ClassUnion // https://tc39.es/ecma262/#prod-ClassIntersection // https://tc39.es/ecma262/#prod-ClassSubtraction pp$1.regexp_classSetExpression = function(state) { var result = CharSetOk, subResult; if (this.regexp_eatClassSetRange(state)) ; else if (subResult = this.regexp_eatClassSetOperand(state)) { if (subResult === CharSetString) { result = CharSetString; } // https://tc39.es/ecma262/#prod-ClassIntersection var start = state.pos; while (state.eatChars([0x26, 0x26] /* && */)) { if ( state.current() !== 0x26 /* & */ && (subResult = this.regexp_eatClassSetOperand(state)) ) { if (subResult !== CharSetString) { result = CharSetOk; } continue } state.raise("Invalid character in character class"); } if (start !== state.pos) { return result } // https://tc39.es/ecma262/#prod-ClassSubtraction while (state.eatChars([0x2D, 0x2D] /* -- */)) { if (this.regexp_eatClassSetOperand(state)) { continue } state.raise("Invalid character in character class"); } if (start !== state.pos) { return result } } else { state.raise("Invalid character in character class"); } // https://tc39.es/ecma262/#prod-ClassUnion for (;;) { if (this.regexp_eatClassSetRange(state)) { continue } subResult = this.regexp_eatClassSetOperand(state); if (!subResult) { return result } if (subResult === CharSetString) { result = CharSetString; } } }; // https://tc39.es/ecma262/#prod-ClassSetRange pp$1.regexp_eatClassSetRange = function(state) { var start = state.pos; if (this.regexp_eatClassSetCharacter(state)) { var left = state.lastIntValue; if (state.eat(0x2D /* - */) && this.regexp_eatClassSetCharacter(state)) { var right = state.lastIntValue; if (left !== -1 && right !== -1 && left > right) { state.raise("Range out of order in character class"); } return true } state.pos = start; } return false }; // https://tc39.es/ecma262/#prod-ClassSetOperand pp$1.regexp_eatClassSetOperand = function(state) { if (this.regexp_eatClassSetCharacter(state)) { return CharSetOk } return this.regexp_eatClassStringDisjunction(state) || this.regexp_eatNestedClass(state) }; // https://tc39.es/ecma262/#prod-NestedClass pp$1.regexp_eatNestedClass = function(state) { var start = state.pos; if (state.eat(0x5B /* [ */)) { var negate = state.eat(0x5E /* ^ */); var result = this.regexp_classContents(state); if (state.eat(0x5D /* ] */)) { if (negate && result === CharSetString) { state.raise("Negated character class may contain strings"); } return result } state.pos = start; } if (state.eat(0x5C /* \ */)) { var result$1 = this.regexp_eatCharacterClassEscape(state); if (result$1) { return result$1 } state.pos = start; } return null }; // https://tc39.es/ecma262/#prod-ClassStringDisjunction pp$1.regexp_eatClassStringDisjunction = function(state) { var start = state.pos; if (state.eatChars([0x5C, 0x71] /* \q */)) { if (state.eat(0x7B /* { */)) { var result = this.regexp_classStringDisjunctionContents(state); if (state.eat(0x7D /* } */)) { return result } } else { // Make the same message as V8. state.raise("Invalid escape"); } state.pos = start; } return null }; // https://tc39.es/ecma262/#prod-ClassStringDisjunctionContents pp$1.regexp_classStringDisjunctionContents = function(state) { var result = this.regexp_classString(state); while (state.eat(0x7C /* | */)) { if (this.regexp_classString(state) === CharSetString) { result = CharSetString; } } return result }; // https://tc39.es/ecma262/#prod-ClassString // https://tc39.es/ecma262/#prod-NonEmptyClassString pp$1.regexp_classString = function(state) { var count = 0; while (this.regexp_eatClassSetCharacter(state)) { count++; } return count === 1 ? CharSetOk : CharSetString }; // https://tc39.es/ecma262/#prod-ClassSetCharacter pp$1.regexp_eatClassSetCharacter = function(state) { var start = state.pos; if (state.eat(0x5C /* \ */)) { if ( this.regexp_eatCharacterEscape(state) || this.regexp_eatClassSetReservedPunctuator(state) ) { return true } if (state.eat(0x62 /* b */)) { state.lastIntValue = 0x08; /* */ return true } state.pos = start; return false } var ch = state.current(); if (ch < 0 || ch === state.lookahead() && isClassSetReservedDoublePunctuatorCharacter(ch)) { return false } if (isClassSetSyntaxCharacter(ch)) { return false } state.advance(); state.lastIntValue = ch; return true }; // https://tc39.es/ecma262/#prod-ClassSetReservedDoublePunctuator function isClassSetReservedDoublePunctuatorCharacter(ch) { return ( ch === 0x21 /* ! */ || ch >= 0x23 /* # */ && ch <= 0x26 /* & */ || ch >= 0x2A /* * */ && ch <= 0x2C /* , */ || ch === 0x2E /* . */ || ch >= 0x3A /* : */ && ch <= 0x40 /* @ */ || ch === 0x5E /* ^ */ || ch === 0x60 /* ` */ || ch === 0x7E /* ~ */ ) } // https://tc39.es/ecma262/#prod-ClassSetSyntaxCharacter function isClassSetSyntaxCharacter(ch) { return ( ch === 0x28 /* ( */ || ch === 0x29 /* ) */ || ch === 0x2D /* - */ || ch === 0x2F /* / */ || ch >= 0x5B /* [ */ && ch <= 0x5D /* ] */ || ch >= 0x7B /* { */ && ch <= 0x7D /* } */ ) } // https://tc39.es/ecma262/#prod-ClassSetReservedPunctuator pp$1.regexp_eatClassSetReservedPunctuator = function(state) { var ch = state.current(); if (isClassSetReservedPunctuator(ch)) { state.lastIntValue = ch; state.advance(); return true } return false }; // https://tc39.es/ecma262/#prod-ClassSetReservedPunctuator function isClassSetReservedPunctuator(ch) { return ( ch === 0x21 /* ! */ || ch === 0x23 /* # */ || ch === 0x25 /* % */ || ch === 0x26 /* & */ || ch === 0x2C /* , */ || ch === 0x2D /* - */ || ch >= 0x3A /* : */ && ch <= 0x3E /* > */ || ch === 0x40 /* @ */ || ch === 0x60 /* ` */ || ch === 0x7E /* ~ */ ) } // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassControlLetter pp$1.regexp_eatClassControlLetter = function(state) { var ch = state.current(); if (isDecimalDigit(ch) || ch === 0x5F /* _ */) { state.lastIntValue = ch % 0x20; state.advance(); return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence pp$1.regexp_eatHexEscapeSequence = function(state) { var start = state.pos; if (state.eat(0x78 /* x */)) { if (this.regexp_eatFixedHexDigits(state, 2)) { return true } if (state.switchU) { state.raise("Invalid escape"); } state.pos = start; } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalDigits pp$1.regexp_eatDecimalDigits = function(state) { var start = state.pos; var ch = 0; state.lastIntValue = 0; while (isDecimalDigit(ch = state.current())) { state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */); state.advance(); } return state.pos !== start }; function isDecimalDigit(ch) { return ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */ } // https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigits pp$1.regexp_eatHexDigits = function(state) { var start = state.pos; var ch = 0; state.lastIntValue = 0; while (isHexDigit(ch = state.current())) { state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch); state.advance(); } return state.pos !== start }; function isHexDigit(ch) { return ( (ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */) || (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) || (ch >= 0x61 /* a */ && ch <= 0x66 /* f */) ) } function hexToInt(ch) { if (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) { return 10 + (ch - 0x41 /* A */) } if (ch >= 0x61 /* a */ && ch <= 0x66 /* f */) { return 10 + (ch - 0x61 /* a */) } return ch - 0x30 /* 0 */ } // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-LegacyOctalEscapeSequence // Allows only 0-377(octal) i.e. 0-255(decimal). pp$1.regexp_eatLegacyOctalEscapeSequence = function(state) { if (this.regexp_eatOctalDigit(state)) { var n1 = state.lastIntValue; if (this.regexp_eatOctalDigit(state)) { var n2 = state.lastIntValue; if (n1 <= 3 && this.regexp_eatOctalDigit(state)) { state.lastIntValue = n1 * 64 + n2 * 8 + state.lastIntValue; } else { state.lastIntValue = n1 * 8 + n2; } } else { state.lastIntValue = n1; } return true } return false }; // https://www.ecma-international.org/ecma-262/8.0/#prod-OctalDigit pp$1.regexp_eatOctalDigit = function(state) { var ch = state.current(); if (isOctalDigit(ch)) { state.lastIntValue = ch - 0x30; /* 0 */ state.advance(); return true } state.lastIntValue = 0; return false }; function isOctalDigit(ch) { return ch >= 0x30 /* 0 */ && ch <= 0x37 /* 7 */ } // https://www.ecma-international.org/ecma-262/8.0/#prod-Hex4Digits // https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigit // And HexDigit HexDigit in https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence pp$1.regexp_eatFixedHexDigits = function(state, length) { var start = state.pos; state.lastIntValue = 0; for (var i = 0; i < length; ++i) { var ch = state.current(); if (!isHexDigit(ch)) { state.pos = start; return false } state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch); state.advance(); } return true }; // Object type used to represent tokens. Note that normally, tokens // simply exist as properties on the parser object. This is only // used for the onToken callback and the external tokenizer. var Token = function Token(p) { this.type = p.type; this.value = p.value; this.start = p.start; this.end = p.end; if (p.options.locations) { this.loc = new SourceLocation(p, p.startLoc, p.endLoc); } if (p.options.ranges) { this.range = [p.start, p.end]; } }; // ## Tokenizer var pp = Parser.prototype; // Move to the next token pp.next = function(ignoreEscapeSequenceInKeyword) { if (!ignoreEscapeSequenceInKeyword && this.type.keyword && this.containsEsc) { this.raiseRecoverable(this.start, "Escape sequence in keyword " + this.type.keyword); } if (this.options.onToken) { this.options.onToken(new Token(this)); } this.lastTokEnd = this.end; this.lastTokStart = this.start; this.lastTokEndLoc = this.endLoc; this.lastTokStartLoc = this.startLoc; this.nextToken(); }; pp.getToken = function() { this.next(); return new Token(this) }; // If we're in an ES6 environment, make parsers iterable if (typeof Symbol !== "undefined") { pp[Symbol.iterator] = function() { var this$1$1 = this; return { next: function () { var token = this$1$1.getToken(); return { done: token.type === types$1.eof, value: token } } } }; } // Toggle strict mode. Re-reads the next number or string to please // pedantic tests (`"use strict"; 010;` should fail). // Read a single token, updating the parser object's token-related // properties. pp.nextToken = function() { var curContext = this.curContext(); if (!curContext || !curContext.preserveSpace) { this.skipSpace(); } this.start = this.pos; if (this.options.locations) { this.startLoc = this.curPosition(); } if (this.pos >= this.input.length) { return this.finishToken(types$1.eof) } if (curContext.override) { return curContext.override(this) } else { this.readToken(this.fullCharCodeAtPos()); } }; pp.readToken = function(code) { // Identifier or keyword. '\uXXXX' sequences are allowed in // identifiers, so '\' also dispatches to that. if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92 /* '\' */) { return this.readWord() } return this.getTokenFromCode(code) }; pp.fullCharCodeAtPos = function() { var code = this.input.charCodeAt(this.pos); if (code <= 0xd7ff || code >= 0xdc00) { return code } var next = this.input.charCodeAt(this.pos + 1); return next <= 0xdbff || next >= 0xe000 ? code : (code << 10) + next - 0x35fdc00 }; pp.skipBlockComment = function() { var startLoc = this.options.onComment && this.curPosition(); var start = this.pos, end = this.input.indexOf("*/", this.pos += 2); if (end === -1) { this.raise(this.pos - 2, "Unterminated comment"); } this.pos = end + 2; if (this.options.locations) { for (var nextBreak = (void 0), pos = start; (nextBreak = nextLineBreak(this.input, pos, this.pos)) > -1;) { ++this.curLine; pos = this.lineStart = nextBreak; } } if (this.options.onComment) { this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos, startLoc, this.curPosition()); } }; pp.skipLineComment = function(startSkip) { var start = this.pos; var startLoc = this.options.onComment && this.curPosition(); var ch = this.input.charCodeAt(this.pos += startSkip); while (this.pos < this.input.length && !isNewLine(ch)) { ch = this.input.charCodeAt(++this.pos); } if (this.options.onComment) { this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos, startLoc, this.curPosition()); } }; // Called at the start of the parse and after every token. Skips // whitespace and comments, and. pp.skipSpace = function() { loop: while (this.pos < this.input.length) { var ch = this.input.charCodeAt(this.pos); switch (ch) { case 32: case 160: // ' ' ++this.pos; break case 13: if (this.input.charCodeAt(this.pos + 1) === 10) { ++this.pos; } case 10: case 8232: case 8233: ++this.pos; if (this.options.locations) { ++this.curLine; this.lineStart = this.pos; } break case 47: // '/' switch (this.input.charCodeAt(this.pos + 1)) { case 42: // '*' this.skipBlockComment(); break case 47: this.skipLineComment(2); break default: break loop } break default: if (ch > 8 && ch < 14 || ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) { ++this.pos; } else { break loop } } } }; // Called at the end of every token. Sets `end`, `val`, and // maintains `context` and `exprAllowed`, and skips the space after // the token, so that the next one's `start` will point at the // right position. pp.finishToken = function(type, val) { this.end = this.pos; if (this.options.locations) { this.endLoc = this.curPosition(); } var prevType = this.type; this.type = type; this.value = val; this.updateContext(prevType); }; // ### Token reading // This is the function that is called to fetch the next token. It // is somewhat obscure, because it works in character codes rather // than characters, and because operator parsing has been inlined // into it. // // All in the name of speed. // pp.readToken_dot = function() { var next = this.input.charCodeAt(this.pos + 1); if (next >= 48 && next <= 57) { return this.readNumber(true) } var next2 = this.input.charCodeAt(this.pos + 2); if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) { // 46 = dot '.' this.pos += 3; return this.finishToken(types$1.ellipsis) } else { ++this.pos; return this.finishToken(types$1.dot) } }; pp.readToken_slash = function() { // '/' var next = this.input.charCodeAt(this.pos + 1); if (this.exprAllowed) { ++this.pos; return this.readRegexp() } if (next === 61) { return this.finishOp(types$1.assign, 2) } return this.finishOp(types$1.slash, 1) }; pp.readToken_mult_modulo_exp = function(code) { // '%*' var next = this.input.charCodeAt(this.pos + 1); var size = 1; var tokentype = code === 42 ? types$1.star : types$1.modulo; // exponentiation operator ** and **= if (this.options.ecmaVersion >= 7 && code === 42 && next === 42) { ++size; tokentype = types$1.starstar; next = this.input.charCodeAt(this.pos + 2); } if (next === 61) { return this.finishOp(types$1.assign, size + 1) } return this.finishOp(tokentype, size) }; pp.readToken_pipe_amp = function(code) { // '|&' var next = this.input.charCodeAt(this.pos + 1); if (next === code) { if (this.options.ecmaVersion >= 12) { var next2 = this.input.charCodeAt(this.pos + 2); if (next2 === 61) { return this.finishOp(types$1.assign, 3) } } return this.finishOp(code === 124 ? types$1.logicalOR : types$1.logicalAND, 2) } if (next === 61) { return this.finishOp(types$1.assign, 2) } return this.finishOp(code === 124 ? types$1.bitwiseOR : types$1.bitwiseAND, 1) }; pp.readToken_caret = function() { // '^' var next = this.input.charCodeAt(this.pos + 1); if (next === 61) { return this.finishOp(types$1.assign, 2) } return this.finishOp(types$1.bitwiseXOR, 1) }; pp.readToken_plus_min = function(code) { // '+-' var next = this.input.charCodeAt(this.pos + 1); if (next === code) { if (next === 45 && !this.inModule && this.input.charCodeAt(this.pos + 2) === 62 && (this.lastTokEnd === 0 || lineBreak.test(this.input.slice(this.lastTokEnd, this.pos)))) { // A `-->` line comment this.skipLineComment(3); this.skipSpace(); return this.nextToken() } return this.finishOp(types$1.incDec, 2) } if (next === 61) { return this.finishOp(types$1.assign, 2) } return this.finishOp(types$1.plusMin, 1) }; pp.readToken_lt_gt = function(code) { // '<>' var next = this.input.charCodeAt(this.pos + 1); var size = 1; if (next === code) { size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2; if (this.input.charCodeAt(this.pos + size) === 61) { return this.finishOp(types$1.assign, size + 1) } return this.finishOp(types$1.bitShift, size) } if (next === 33 && code === 60 && !this.inModule && this.input.charCodeAt(this.pos + 2) === 45 && this.input.charCodeAt(this.pos + 3) === 45) { // ` pow, but make it stay pow in // GLSL instead of turning it back into ** } } function nodeIsUniform(ancestor) { return ancestor.type === 'CallExpression' && ( ( // Global mode ancestor.callee?.type === 'Identifier' && ancestor.callee?.name.startsWith('uniform') ) || ( // Instance mode ancestor.callee?.type === 'MemberExpression' && ancestor.callee?.property.name.startsWith('uniform') ) ); } function nodeIsVarying(node) { return node?.type === 'CallExpression' && ( ( // Global mode node.callee?.type === 'Identifier' && (node.callee?.name.startsWith('varying') || node.callee?.name.startsWith('shared')) ) || ( // Instance mode node.callee?.type === 'MemberExpression' && (node.callee?.property.name.startsWith('varying') || node.callee?.property.name.startsWith('shared')) ) ); } // Helper function to check if a statement is a variable declaration with strands control flow init function statementContainsStrandsControlFlow(stmt) { // Check for variable declarations with strands control flow init if (stmt.type === 'VariableDeclaration') { const match = stmt.declarations.some(decl => decl.init?.type === 'CallExpression' && ( ( decl.init?.callee?.type === 'MemberExpression' && decl.init?.callee?.object?.type === 'Identifier' && decl.init?.callee?.object?.name === '__p5' && (decl.init?.callee?.property?.name === 'strandsFor' || decl.init?.callee?.property?.name === 'strandsIf') ) || ( decl.init?.callee?.type === 'Identifier' && (decl.init?.callee?.name === '__p5.strandsFor' || decl.init?.callee?.name === '__p5.strandsIf') ) ) ); return match } return false; } // Helper function to build property path from MemberExpression // e.g., inputs.color -> "inputs.color", vec.x -> "vec.x" function isSwizzle(propertyName) { if (!propertyName || typeof propertyName !== 'string') return false; const swizzleSets = [ ['x', 'y', 'z', 'w'], ['r', 'g', 'b', 'a'], ['s', 't', 'p', 'q'] ]; return swizzleSets.some(set => [...propertyName].every(char => set.includes(char)) ); } function buildPropertyPath(memberExpr) { const parts = []; let current = memberExpr; while (current.type === 'MemberExpression') { if (current.computed) { return null; } const propName = current.property.name || current.property.value; if (isSwizzle(propName)) { current = current.object; break; } parts.unshift(propName); current = current.object; } if (current.type === 'Identifier') { parts.unshift(current.name); } else { return null; } return parts.join('.'); } // Replace all references to original variables with temp variables // and wrap literal assignments in strandsNode calls function replaceReferences(node, tempVarMap) { const internalReplaceReferences = (node) => { if (!node || typeof node !== 'object') return; // Check if this MemberExpression matches a tracked property path if (node.type === 'MemberExpression') { const propName = node.property.name || node.property.value; if (isSwizzle(propName)) { // For swizzles, only replace the object part, keep the swizzle internalReplaceReferences(node.object); return; } const propertyPath = buildPropertyPath(node); if (propertyPath && tempVarMap.has(propertyPath)) { // Replace entire member expression with temp variable Object.assign(node, { type: 'Identifier', name: tempVarMap.get(propertyPath) }); return; // Don't recurse into replaced node } } // Handle simple identifier replacements if (node.type === 'Identifier' && tempVarMap.has(node.name)) { node.name = tempVarMap.get(node.name); } // Handle literal assignments to temp variables if (node.type === 'AssignmentExpression') { let leftPath = null; if (node.left.type === 'Identifier') { leftPath = node.left.name; } else if (node.left.type === 'MemberExpression') { leftPath = buildPropertyPath(node.left); } if (leftPath && tempVarMap.has(leftPath) && (node.right.type === 'Literal' || node.right.type === 'ArrayExpression')) { // Wrap the right hand side in a strandsNode call to make sure // it's not just a literal and has a type node.right = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsNode' }, arguments: [node.right] }; } } // Recursively process all properties for (const key in node) { if (node.hasOwnProperty(key) && key !== 'parent') { // Don't recurse into property names of non-computed member expressions if (node.type === 'MemberExpression' && key === 'property' && !node.computed) { continue; } if (Array.isArray(node[key])) { node[key].forEach(internalReplaceReferences); } else if (typeof node[key] === 'object') { internalReplaceReferences(node[key]); } } } }; internalReplaceReferences(node); } const ASTCallbacks = { UnaryExpression(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } const unaryFnName = UnarySymbolToName[node.operator]; const standardReplacement = (node) => { node.type = 'CallExpression'; node.callee = { type: 'Identifier', name: `__p5.${unaryFnName}`, }; node.arguments = [node.argument]; }; if (node.type === 'MemberExpression') { const property = node.argument.property.name; const swizzleSets = [ ['x', 'y', 'z', 'w'], ['r', 'g', 'b', 'a'], ['s', 't', 'p', 'q'] ]; let isSwizzle = swizzleSets.some(set => [...property].every(char => set.includes(char)) ) && node.argument.type === 'MemberExpression'; if (isSwizzle) { node.type = 'MemberExpression'; node.object = { type: 'CallExpression', callee: { type: 'Identifier', name: `__p5.${unaryFnName}` }, arguments: [node.argument.object], }; node.property = { type: 'Identifier', name: property }; } else { standardReplacement(node); } } else { standardReplacement(node); } delete node.argument; delete node.operator; }, BreakStatement(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } node.callee = { type: 'Identifier', name: '__p5.break' }; node.arguments = []; node.type = 'CallExpression'; }, VariableDeclarator(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } if (nodeIsUniform(node.init)) { // Only inject the variable name if the first argument isn't already a string if (node.init.arguments.length === 0 || node.init.arguments[0].type !== 'Literal' || typeof node.init.arguments[0].value !== 'string') { const uniformNameLiteral = { type: 'Literal', value: node.id.name }; node.init.arguments.unshift(uniformNameLiteral); } } if (nodeIsVarying(node.init)) { // Only inject the variable name if the first argument isn't already a string if ( node.init.arguments.length === 0 || node.init.arguments[0].type !== 'Literal' || typeof node.init.arguments[0].value !== 'string' ) { const varyingNameLiteral = { type: 'Literal', value: node.id.name }; node.init.arguments.unshift(varyingNameLiteral); _state.varyings[node.id.name] = varyingNameLiteral; } else { // Still track it as a varying even if name wasn't injected _state.varyings[node.id.name] = node.init.arguments[0]; } } }, Identifier(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } if (_state.varyings[node.name] && !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) { node.type = 'CallExpression'; node.callee = { type: 'MemberExpression', object: { type: 'Identifier', name: node.name }, property: { type: 'Identifier', name: 'getValue' }, }; node.arguments = []; } }, // The callbacks for AssignmentExpression and BinaryExpression handle // operator overloading including +=, *= assignment expressions ArrayExpression(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } const original = JSON.parse(JSON.stringify(node)); node.type = 'CallExpression'; node.callee = { type: 'Identifier', name: '__p5.strandsNode', }; node.arguments = [original]; }, AssignmentExpression(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; if (node.operator !== '=') { const methodName = replaceBinaryOperator(node.operator.replace('=','')); const rightReplacementNode = { type: 'CallExpression', callee: { type: 'MemberExpression', object: unsafeTypes.includes(node.left.type) ? { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsNode', }, arguments: [node.left] } : node.left, property: { type: 'Identifier', name: methodName, }, }, arguments: [node.right] }; node.operator = '='; node.right = rightReplacementNode; } // Handle direct varying variable assignment: myVarying = value if (_state.varyings[node.left.name]) { node.type = 'ExpressionStatement'; node.expression = { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'Identifier', name: node.left.name }, property: { type: 'Identifier', name: 'bridge', } }, arguments: [node.right], }; } // Handle swizzle assignment to varying variable: myVarying.xyz = value // Note: node.left.object might be worldPos.getValue() due to prior Identifier transformation else if (node.left.type === 'MemberExpression') { let varyingName = null; // Check if it's a direct identifier: myVarying.xyz if (node.left.object.type === 'Identifier' && _state.varyings[node.left.object.name]) { varyingName = node.left.object.name; } // Check if it's a getValue() call: myVarying.getValue().xyz else if (node.left.object.type === 'CallExpression' && node.left.object.callee?.type === 'MemberExpression' && node.left.object.callee.property?.name === 'getValue' && node.left.object.callee.object?.type === 'Identifier' && _state.varyings[node.left.object.callee.object.name]) { varyingName = node.left.object.callee.object.name; } if (varyingName) { const swizzlePattern = node.left.property.name; node.type = 'ExpressionStatement'; node.expression = { type: 'CallExpression', callee: { type: 'MemberExpression', object: { type: 'Identifier', name: varyingName }, property: { type: 'Identifier', name: 'bridgeSwizzle', } }, arguments: [ { type: 'Literal', value: swizzlePattern }, node.right ], }; } } }, BinaryExpression(node, _state, ancestors) { // Don't convert uniform default values to node methods, as // they should be evaluated at runtime, not compiled. if (ancestors.some(nodeIsUniform)) { return; } // If the left hand side of an expression is one of these types, // we should construct a node from it. const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; if (unsafeTypes.includes(node.left.type)) { const leftReplacementNode = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsNode', }, arguments: [node.left] }; node.left = leftReplacementNode; } // Replace the binary operator with a call expression // in other words a call to BaseNode.mult(), .div() etc. node.type = 'CallExpression'; node.callee = { type: 'MemberExpression', object: node.left, property: { type: 'Identifier', name: replaceBinaryOperator(node.operator), }, }; node.arguments = [node.right]; }, LogicalExpression(node, _state, ancestors) { // Don't convert uniform default values to node methods, as // they should be evaluated at runtime, not compiled. if (ancestors.some(nodeIsUniform)) { return; } // If the left hand side of an expression is one of these types, // we should construct a node from it. const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; if (unsafeTypes.includes(node.left.type)) { const leftReplacementNode = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsNode', }, arguments: [node.left] }; node.left = leftReplacementNode; } // Replace the logical operator with a call expression // in other words a call to BaseNode.or(), .and() etc. node.type = 'CallExpression'; node.callee = { type: 'MemberExpression', object: node.left, property: { type: 'Identifier', name: replaceBinaryOperator(node.operator), }, }; node.arguments = [node.right]; }, IfStatement(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } // Transform if statement into strandsIf() call // The condition is evaluated directly, not wrapped in a function const condition = node.test; // Create the then function const thenFunction = { type: 'ArrowFunctionExpression', params: [], body: node.consequent.type === 'BlockStatement' ? node.consequent : { type: 'BlockStatement', body: [node.consequent] } }; // Start building the call chain: __p5.strandsIf(condition, then) let callExpression = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsIf' }, arguments: [condition, thenFunction] }; // Always chain .Else() even if there's no explicit else clause // This ensures the conditional completes and returns phi nodes let elseFunction; if (node.alternate) { elseFunction = { type: 'ArrowFunctionExpression', params: [], body: node.alternate.type === 'BlockStatement' ? node.alternate : { type: 'BlockStatement', body: [node.alternate] } }; } else { // Create an empty else function elseFunction = { type: 'ArrowFunctionExpression', params: [], body: { type: 'BlockStatement', body: [] } }; } callExpression = { type: 'CallExpression', callee: { type: 'MemberExpression', object: callExpression, property: { type: 'Identifier', name: 'Else' } }, arguments: [elseFunction] }; // Analyze which outer scope variables are assigned in any branch const assignedVars = new Set(); const analyzeBranch = (functionBody) => { // First pass: collect all variable declarations in the branch const localVars = new Set(); ancestor(functionBody, { VariableDeclarator(node, ancestors) { // Skip if we're inside a block that contains strands control flow if (ancestors.some(statementContainsStrandsControlFlow)) return; if (node.id.type === 'Identifier') { localVars.add(node.id.name); } } }); // Second pass: find assignments to non-local variables using acorn-walk ancestor(functionBody, { AssignmentExpression(node, ancestors) { // Skip if we're inside a block that contains strands control flow if (ancestors.some(statementContainsStrandsControlFlow)) return; const left = node.left; if (left.type === 'Identifier') { // Direct variable assignment: x = value if (!localVars.has(left.name)) { assignedVars.add(left.name); } } else if (left.type === 'MemberExpression') { // Property assignment: obj.prop = value or obj.a.b = value const propertyPath = buildPropertyPath(left); if (propertyPath) { const baseName = propertyPath.split('.')[0]; if (!localVars.has(baseName)) { assignedVars.add(propertyPath); } } } } }); }; // Analyze all branches for assignments to outer scope variables analyzeBranch(thenFunction.body); analyzeBranch(elseFunction.body); if (assignedVars.size > 0) { // Add copying, reference replacement, and return statements to branch functions const addCopyingAndReturn = (functionBody, varsToReturn) => { if (functionBody.type === 'BlockStatement') { // Create temporary variables and copy statements const tempVarMap = new Map(); // property path -> temp name const copyStatements = []; for (const varPath of varsToReturn) { const parts = varPath.split('.'); const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`; tempVarMap.set(varPath, tempName); // Build the member expression for the property path let sourceExpr = { type: 'Identifier', name: parts[0] }; for (let i = 1; i < parts.length; i++) { sourceExpr = { type: 'MemberExpression', object: sourceExpr, property: { type: 'Identifier', name: parts[i] }, computed: false }; } // let tempName = propertyPath.copy() copyStatements.push({ type: 'VariableDeclaration', declarations: [{ type: 'VariableDeclarator', id: { type: 'Identifier', name: tempName }, init: { type: 'CallExpression', callee: { type: 'MemberExpression', object: sourceExpr, property: { type: 'Identifier', name: 'copy' }, computed: false }, arguments: [] } }], kind: 'let' }); } // Apply reference replacement to all statements functionBody.body.forEach(node => replaceReferences(node, tempVarMap)); // Insert copy statements at the beginning functionBody.body.unshift(...copyStatements); // Add return statement with flat object using property paths as keys const returnObj = { type: 'ObjectExpression', properties: Array.from(varsToReturn).map(varPath => ({ type: 'Property', key: { type: 'Literal', value: varPath }, value: { type: 'Identifier', name: tempVarMap.get(varPath) }, kind: 'init', computed: false, shorthand: false })) }; functionBody.body.push({ type: 'ReturnStatement', argument: returnObj }); } }; addCopyingAndReturn(thenFunction.body, assignedVars); addCopyingAndReturn(elseFunction.body, assignedVars); // Create a block variable to capture the return value const blockVar = `__block_${blockVarCounter++}`; // Replace with a block statement const statements = []; // Make sure every assigned variable starts as a node for (const varPath of assignedVars) { const parts = varPath.split('.'); // Build left side: inputs.color or just x let leftExpr = { type: 'Identifier', name: parts[0] }; for (let i = 1; i < parts.length; i++) { leftExpr = { type: 'MemberExpression', object: leftExpr, property: { type: 'Identifier', name: parts[i] }, computed: false }; } // Build right side - same as left for strandsNode wrapping let rightArgExpr = { type: 'Identifier', name: parts[0] }; for (let i = 1; i < parts.length; i++) { rightArgExpr = { type: 'MemberExpression', object: rightArgExpr, property: { type: 'Identifier', name: parts[i] }, computed: false }; } statements.push({ type: 'ExpressionStatement', expression: { type: 'AssignmentExpression', operator: '=', left: leftExpr, right: { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsNode' }, arguments: [rightArgExpr], } } }); } statements.push({ type: 'VariableDeclaration', declarations: [{ type: 'VariableDeclarator', id: { type: 'Identifier', name: blockVar }, init: callExpression }], kind: 'const' }); // 2. Assignments for each modified variable for (const varPath of assignedVars) { const parts = varPath.split('.'); // Build left side: inputs.color or just x let leftExpr = { type: 'Identifier', name: parts[0] }; for (let i = 1; i < parts.length; i++) { leftExpr = { type: 'MemberExpression', object: leftExpr, property: { type: 'Identifier', name: parts[i] }, computed: false }; } // Build right side: __block_2['inputs.color'] or __block_2['x'] const rightExpr = { type: 'MemberExpression', object: { type: 'Identifier', name: blockVar }, property: { type: 'Literal', value: varPath }, computed: true }; statements.push({ type: 'ExpressionStatement', expression: { type: 'AssignmentExpression', operator: '=', left: leftExpr, right: rightExpr } }); } // Replace the if statement with a block statement node.type = 'BlockStatement'; node.body = statements; } else { // No assignments, just replace with the call expression node.type = 'ExpressionStatement'; node.expression = callExpression; } delete node.test; delete node.consequent; delete node.alternate; }, UpdateExpression(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } // Transform ++var, var++, --var, var-- into assignment expressions let operator; if (node.operator === '++') { operator = '+'; } else if (node.operator === '--') { operator = '-'; } else { return; // Unknown update operator } // Convert to: var = var + 1 or var = var - 1 const assignmentExpr = { type: 'AssignmentExpression', operator: '=', left: node.argument, right: { type: 'BinaryExpression', operator: operator, left: node.argument, right: { type: 'Literal', value: 1 } } }; // Replace the update expression with the assignment expression Object.assign(node, assignmentExpr); delete node.prefix; this.BinaryExpression(node.right, _state, [...ancestors, node]); this.AssignmentExpression(node, _state, ancestors); }, ForStatement(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } // Transform for statement into strandsFor() call // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars) // Generate unique loop variable name const uniqueLoopVar = `loopVar${loopVarCounter++}`; // Create the initial callback from the for loop's init let initialFunction; if (node.init && node.init.type === 'VariableDeclaration') { // Handle: for (let i = 0; ...) const declaration = node.init.declarations[0]; let initValue = declaration.init; const initAst = { body: [{ type: 'ExpressionStatement', expression: initValue }] }; initValue = initAst.body[0].expression; initialFunction = { type: 'ArrowFunctionExpression', params: [], body: { type: 'BlockStatement', body: [{ type: 'ReturnStatement', argument: initValue }] } }; } else { // Handle other cases - return a default value initialFunction = { type: 'ArrowFunctionExpression', params: [], body: { type: 'BlockStatement', body: [{ type: 'ReturnStatement', argument: { type: 'Literal', value: 0 } }] } }; } // Create the condition callback let conditionBody = node.test || { type: 'Literal', value: true }; // Replace loop variable references with the parameter if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar); } const conditionAst = { body: [{ type: 'ExpressionStatement', expression: conditionBody }] }; conditionBody = conditionAst.body[0].expression; const conditionFunction = { type: 'ArrowFunctionExpression', params: [{ type: 'Identifier', name: uniqueLoopVar }], body: conditionBody }; // Create the update callback let updateFunction; if (node.update) { let updateExpr = node.update; // Replace loop variable references with the parameter if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar); } const updateAst = { body: [{ type: 'ExpressionStatement', expression: updateExpr }] }; updateExpr = updateAst.body[0].expression; updateFunction = { type: 'ArrowFunctionExpression', params: [{ type: 'Identifier', name: uniqueLoopVar }], body: { type: 'BlockStatement', body: [{ type: 'ReturnStatement', argument: updateExpr }] } }; } else { updateFunction = { type: 'ArrowFunctionExpression', params: [{ type: 'Identifier', name: uniqueLoopVar }], body: { type: 'BlockStatement', body: [{ type: 'ReturnStatement', argument: { type: 'Identifier', name: uniqueLoopVar } }] } }; } // Create the body callback let bodyBlock = node.body.type === 'BlockStatement' ? node.body : { type: 'BlockStatement', body: [node.body] }; // Replace loop variable references in the body if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar); } const bodyFunction = { type: 'ArrowFunctionExpression', params: [ { type: 'Identifier', name: uniqueLoopVar }, { type: 'Identifier', name: 'vars' } ], body: bodyBlock }; // Analyze which outer scope variables are assigned in the loop body const assignedVars = new Set(); // First pass: collect all variable declarations in the body const localVars = new Set(); ancestor(bodyFunction.body, { VariableDeclarator(node, ancestors) { // Skip if we're inside a block that contains strands control flow if (ancestors.some(statementContainsStrandsControlFlow)) return; if (node.id.type === 'Identifier') { localVars.add(node.id.name); } } }); // Second pass: find assignments to non-local variables using acorn-walk ancestor(bodyFunction.body, { AssignmentExpression(node, ancestors) { // Skip if we're inside a block that contains strands control flow if (ancestors.some(statementContainsStrandsControlFlow)) { return } const left = node.left; if (left.type === 'Identifier') { // Direct variable assignment: x = value if (!localVars.has(left.name)) { assignedVars.add(left.name); } } else if (left.type === 'MemberExpression') { // Property assignment: obj.prop = value or obj.a.b = value const propertyPath = buildPropertyPath(left); if (propertyPath) { const baseName = propertyPath.split('.')[0]; if (!localVars.has(baseName)) { assignedVars.add(propertyPath); } } } } }); if (assignedVars.size > 0) { // Add copying, reference replacement, and return statements similar to if statements const addCopyingAndReturn = (functionBody, varsToReturn) => { if (functionBody.type === 'BlockStatement') { const tempVarMap = new Map(); const copyStatements = []; for (const varPath of varsToReturn) { const parts = varPath.split('.'); const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`; tempVarMap.set(varPath, tempName); // Build the member expression for vars.propertyPath // e.g., vars.inputs.color or vars.x let sourceExpr = { type: 'Identifier', name: 'vars' }; for (const part of parts) { sourceExpr = { type: 'MemberExpression', object: sourceExpr, property: { type: 'Identifier', name: part }, computed: false }; } copyStatements.push({ type: 'VariableDeclaration', declarations: [{ type: 'VariableDeclarator', id: { type: 'Identifier', name: tempName }, init: { type: 'CallExpression', callee: { type: 'MemberExpression', object: sourceExpr, property: { type: 'Identifier', name: 'copy' }, computed: false }, arguments: [] } }], kind: 'let' }); } functionBody.body.forEach(node => replaceReferences(node, tempVarMap)); functionBody.body.unshift(...copyStatements); // Add return statement with flat object using property paths as keys const returnObj = { type: 'ObjectExpression', properties: Array.from(varsToReturn).map(varPath => ({ type: 'Property', key: { type: 'Literal', value: varPath }, value: { type: 'Identifier', name: tempVarMap.get(varPath) }, kind: 'init', computed: false, shorthand: false })) }; functionBody.body.push({ type: 'ReturnStatement', argument: returnObj }); } }; addCopyingAndReturn(bodyFunction.body, assignedVars); // Create block variable and assignments similar to if statements const blockVar = `__block_${blockVarCounter++}`; const statements = []; const initialVarsObject = { type: 'ObjectExpression', properties: Array.from(assignedVars).map(varPath => { const parts = varPath.split('.'); let expr = { type: 'Identifier', name: parts[0] }; for (let i = 1; i < parts.length; i++) { expr = { type: 'MemberExpression', object: expr, property: { type: 'Identifier', name: parts[i] }, computed: false }; } const wrappedExpr = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsNode' }, arguments: [expr] }; return { type: 'Property', key: { type: 'Literal', value: varPath }, value: wrappedExpr, kind: 'init', computed: false, shorthand: false }; }) }; // Create the strandsFor call const callExpression = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsFor' }, arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, initialVarsObject] }; statements.push({ type: 'VariableDeclaration', declarations: [{ type: 'VariableDeclarator', id: { type: 'Identifier', name: blockVar }, init: callExpression }], kind: 'const' }); // Add assignments back to original variables for (const varPath of assignedVars) { const parts = varPath.split('.'); // Build left side: inputs.color or just x let leftExpr = { type: 'Identifier', name: parts[0] }; for (let i = 1; i < parts.length; i++) { leftExpr = { type: 'MemberExpression', object: leftExpr, property: { type: 'Identifier', name: parts[i] }, computed: false }; } // Build right side: __block_2.inputs.color or __block_2.x let rightExpr = { type: 'Identifier', name: blockVar }; for (const part of parts) { rightExpr = { type: 'MemberExpression', object: rightExpr, property: { type: 'Identifier', name: part }, computed: false }; } statements.push({ type: 'ExpressionStatement', expression: { type: 'AssignmentExpression', operator: '=', left: leftExpr, right: rightExpr } }); } node.type = 'BlockStatement'; node.body = statements; } else { // No assignments, just replace with call expression node.type = 'ExpressionStatement'; node.expression = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsFor' }, arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, { type: 'ObjectExpression', properties: [] }] }; } delete node.init; delete node.test; delete node.update; }, // Helper method to replace identifier references in AST nodes replaceIdentifierReferences(node, oldName, newName) { if (!node || typeof node !== 'object') return node; const replaceInNode = (n) => { if (!n || typeof n !== 'object') return n; if (n.type === 'Identifier' && n.name === oldName) { return { ...n, name: newName }; } // Create a copy and recursively process properties const newNode = { ...n }; for (const key in n) { if (n.hasOwnProperty(key) && key !== 'parent') { if (Array.isArray(n[key])) { newNode[key] = n[key].map(replaceInNode); } else if (typeof n[key] === 'object') { newNode[key] = replaceInNode(n[key]); } } } return newNode; }; return replaceInNode(node); } }; function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { // Reset counters at the start of each transpilation blockVarCounter = 0; loopVarCounter = 0; const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations }); // First pass: transform everything except if/for statements using normal ancestor traversal const nonControlFlowCallbacks = { ...ASTCallbacks }; delete nonControlFlowCallbacks.IfStatement; delete nonControlFlowCallbacks.ForStatement; ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} }); // Second pass: transform if/for statements in post-order using recursive traversal const postOrderControlFlowTransform = { IfStatement(node, state, c) { state.inControlFlow++; // First recursively process children if (node.test) c(node.test, state); if (node.consequent) c(node.consequent, state); if (node.alternate) c(node.alternate, state); // Then apply the transformation to this node ASTCallbacks.IfStatement(node, state, []); state.inControlFlow--; }, ForStatement(node, state, c) { state.inControlFlow++; // First recursively process children if (node.init) c(node.init, state); if (node.test) c(node.test, state); if (node.update) c(node.update, state); if (node.body) c(node.body, state); // Then apply the transformation to this node ASTCallbacks.ForStatement(node, state, []); state.inControlFlow--; }, ReturnStatement(node, state, c) { if (!state.inControlFlow) return; // Convert return statement to strandsEarlyReturn call node.type = 'ExpressionStatement'; node.expression = { type: 'CallExpression', callee: { type: 'Identifier', name: '__p5.strandsEarlyReturn' }, arguments: node.argument ? [node.argument] : [] }; delete node.argument; } }; recursive(ast, { varyings: {}, inControlFlow: 0 }, postOrderControlFlowTransform); const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); const match = /\(?\s*(?:function)?\s*\w*\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/ .exec(transpiledSource); if (!match) { console.log(transpiledSource); throw new Error('Could not parse p5.strands function!'); } const params = match[1].split(/,\s*/).filter(param => !!param.trim()); let paramVals, paramNames; if (params.length > 0) { paramNames = params; paramVals = [scope]; } else { paramNames = scopeKeys; paramVals = scopeKeys.map(key => scope[key]); } const body = match[2]; try { const internalStrandsCallback = new Function( // Create a parameter called __p5, not just p5, because users of instance mode // may pass in a variable called p5 as a scope variable. If we rely on a variable called // p5, then the scope variable called p5 might accidentally override internal function // calls to p5 static methods. '__p5', ...paramNames, body, ); return () => internalStrandsCallback(p5, ...paramVals); } catch (e) { console.error(e); console.log(paramNames); console.log(body); throw new Error('Error transpiling p5.strands callback!'); } } function generateShaderCode(strandsContext) { const { cfg, backend, vertexDeclarations, fragmentDeclarations } = strandsContext; const hooksObj = { uniforms: {}, varyingVariables: [], }; for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) { const key = backend.generateHookUniformKey(name, typeInfo); if (key !== null) { hooksObj.uniforms[key] = defaultValue; } } // Add texture bindings to declarations for WebGPU backend if (backend.addTextureBindingsToDeclarations) { backend.addTextureBindingsToDeclarations(strandsContext); } for (const { hookType, rootNodeID, entryBlockID, shaderContext } of strandsContext.hooks) { const generationContext = { indent: 1, codeLines: [], write(line) { this.codeLines.push(' '.repeat(this.indent) + line); }, tempNames: {}, declarations: [], nextTempID: 0, visitedNodes: new Set(), shaderContext, // 'vertex' or 'fragment' strandsContext, // For shared variable tracking }; const blocks = sortCFG(cfg.outgoingEdges, entryBlockID); for (const blockID of blocks) { backend.generateBlock(blockID, strandsContext, generationContext); } // Process any unvisited global assignments to ensure side effects are generated for (const assignmentNodeID of strandsContext.globalAssignments) { if (!generationContext.visitedNodes.has(assignmentNodeID)) { // This assignment hasn't been visited yet, so we need to generate it backend.generateAssignment(generationContext, strandsContext.dag, assignmentNodeID); generationContext.visitedNodes.add(assignmentNodeID); } } // Reset global assignments for next hook strandsContext.globalAssignments = []; const firstLine = backend.hookEntry(hookType); let returnType; if (hookType.returnType.properties) { returnType = structType(hookType.returnType); } else { if (!hookType.returnType.dataType) { throw new Error(`Missing dataType for return type ${hookType.returnType.typeName}`); } returnType = hookType.returnType.dataType; } if (rootNodeID) { backend.generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType); } hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); } // Finalize shared variable declarations based on usage if (strandsContext.sharedVariables) { for (const [varName, varInfo] of strandsContext.sharedVariables) { if (varInfo.usedInVertex && varInfo.usedInFragment) { // Used in both shaders - this is a true varying variable hooksObj.varyingVariables.push(backend.generateVaryingVariable(varName, varInfo.typeInfo)); } else if (varInfo.usedInVertex) { // Only used in vertex shader - declare as local variable vertexDeclarations.add(backend.generateLocalDeclaration(varName, varInfo.typeInfo)); } else if (varInfo.usedInFragment) { // Only used in fragment shader - declare as local variable fragmentDeclarations.add(backend.generateLocalDeclaration(varName, varInfo.typeInfo)); } // If not used anywhere, don't declare it } } hooksObj.vertexDeclarations = [...vertexDeclarations].join('\n'); hooksObj.fragmentDeclarations = [...fragmentDeclarations].join('\n'); return hooksObj; } function createPhiNode(strandsContext, phiInputs, varName) { // Determine the proper dimension and baseType from the inputs const validInputs = phiInputs.filter(input => input.value.id !== null); if (validInputs.length === 0) { throw new Error(`No valid inputs for phi node for variable ${varName}`); } // Get dimension and baseType from first valid input let firstInput = validInputs .map((input) => getNodeDataFromID(strandsContext.dag, input.value.id)) .find((input) => input.dimension) ?? getNodeDataFromID(strandsContext.dag, validInputs[0].value.id); const dimension = firstInput.dimension; const baseType = firstInput.baseType; const nodeData = { nodeType: NodeType.PHI, dimension, baseType, dependsOn: phiInputs.map(input => input.value.id).filter(id => id !== null), phiBlocks: phiInputs.map(input => input.blockId)}; const id = getOrCreateNode(strandsContext.dag, nodeData); recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); return { id, dimension, baseType }; } class StrandsConditional { constructor(strandsContext, condition, branchCallback) { // Condition must be a node... this.branches = [{ condition, branchCallback, blockType: BlockType.IF_BODY }]; this.ctx = strandsContext; } ElseIf(condition, branchCallback) { this.branches.push({ condition, branchCallback, blockType: BlockType.IF_BODY }); return this; } Else(branchCallback = () => ({})) { this.branches.push({ condition: null, branchCallback, blockType: BlockType.IF_BODY }); const phiNodes = buildConditional(this.ctx, this); const assignments = {}; for (const [varName, phiNode] of Object.entries(phiNodes)) { assignments[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.ctx); } return assignments; } } function buildConditional(strandsContext, conditional) { const cfg = strandsContext.cfg; const branches = conditional.branches; const mergeBlock = createBasicBlock(cfg, BlockType.MERGE); const results = []; const branchEndBlocks = []; const mergedAssignments = {}; const phiBlockDependencies = {}; // Create a BRANCH block to handle phi node declarations const branchBlock = createBasicBlock(cfg, BlockType.BRANCH); addEdge(cfg, cfg.currentBlock, branchBlock); addEdge(cfg, branchBlock, mergeBlock); let previousBlock = branchBlock; for (let i = 0; i < branches.length; i++) { const { condition, branchCallback, blockType } = branches[i]; if (condition !== null) { const conditionBlock = createBasicBlock(cfg, BlockType.IF_COND); addEdge(cfg, previousBlock, conditionBlock); pushBlock(cfg, conditionBlock); cfg.blockConditions[conditionBlock] = condition.id; previousBlock = conditionBlock; popBlock(cfg); } else { const elseCondBlock = createBasicBlock(cfg, BlockType.ELSE_COND); addEdge(cfg, previousBlock, elseCondBlock); previousBlock = elseCondBlock; } const scopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START); addEdge(cfg, previousBlock, scopeStartBlock); const branchContentBlock = createBasicBlock(cfg, blockType); addEdge(cfg, scopeStartBlock, branchContentBlock); pushBlock(cfg, branchContentBlock); const branchResults = branchCallback(); for (const key in branchResults) { branchResults[key] = strandsContext.p5.strandsNode(branchResults[key]); } for (const key in branchResults) { if (!phiBlockDependencies[key]) { phiBlockDependencies[key] = [{ value: branchResults[key], blockId: branchContentBlock }]; } else { phiBlockDependencies[key].push({ value: branchResults[key], blockId: branchContentBlock }); } } results.push(branchResults); // Create BRANCH_END block for phi assignments const branchEndBlock = createBasicBlock(cfg, BlockType.DEFAULT); addEdge(cfg, cfg.currentBlock, branchEndBlock); branchEndBlocks.push(branchEndBlock); popBlock(cfg); const scopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END); addEdge(cfg, branchEndBlock, scopeEndBlock); addEdge(cfg, scopeEndBlock, mergeBlock); previousBlock = scopeStartBlock; } // Push the branch block for modification to avoid changing the ordering pushBlockForModification(cfg, branchBlock); for (const key in phiBlockDependencies) { mergedAssignments[key] = createPhiNode(strandsContext, phiBlockDependencies[key], key); } popBlock(cfg); for (let i = 0; i < results.length; i++) { const branchResult = results[i]; const branchEndBlockID = branchEndBlocks[i]; pushBlockForModification(cfg, branchEndBlockID); for (const key in branchResult) { if (mergedAssignments[key]) { // Create an assignment statement: phiNode = branchResult[key] const phiNodeID = mergedAssignments[key].id; const sourceNodeID = branchResult[key].id; // Create an assignment operation node // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode) const assignmentNode = { nodeType: NodeType.ASSIGNMENT, dependsOn: [phiNodeID, sourceNodeID], phiBlocks: [] }; const assignmentID = getOrCreateNode(strandsContext.dag, assignmentNode); recordInBasicBlock(cfg, branchEndBlockID, assignmentID); } } popBlock(cfg); } pushBlock(cfg, mergeBlock); return mergedAssignments; } class StrandsFor { constructor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars) { this.strandsContext = strandsContext; this.initialCb = initialCb; this.conditionCb = conditionCb; this.updateCb = updateCb; this.bodyCb = bodyCb; this.initialVars = initialVars; } build() { const cfg = this.strandsContext.cfg; const mergeBlock = createBasicBlock(cfg, BlockType.MERGE); // Create a BRANCH block to handle phi node declarations const branchBlock = createBasicBlock(cfg, BlockType.BRANCH); addEdge(cfg, cfg.currentBlock, branchBlock); addEdge(cfg, branchBlock, mergeBlock); // Initialize loop variable phi node const { initialVar, phiNode } = this.initializeLoopVariable(cfg, branchBlock); // Execute condition and update callbacks to get nodes for analysis pushBlock(cfg, cfg.currentBlock); const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext); const conditionNode = this.conditionCb(loopVarNode); const updateResult = this.updateCb(loopVarNode); popBlock(cfg); // Check if loop has bounded iteration count const isBounded = this.loopIsBounded(initialVar, conditionNode, updateResult); if (isBounded) { this.buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult); } else { this.buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult); } // Update the phi nodes created in buildBoundedLoop with actual body results const finalPhiNodes = this.phiNodesForBody; pushBlockForModification(cfg, branchBlock); for (const [varName, resultNode] of Object.entries(this.bodyResults)) { if (varName !== 'loopVar' && finalPhiNodes[varName]) { // Update the phi node's second input to use the actual body result const phiNodeID = finalPhiNodes[varName].id; const phiNodeData = getNodeDataFromID(this.strandsContext.dag, phiNodeID); // Update the dependsOn array to include the actual body result if (phiNodeData.dependsOn.length > 1) { phiNodeData.dependsOn[1] = resultNode.id; } if (phiNodeData.phiInputs && phiNodeData.phiInputs.length > 1) { phiNodeData.phiInputs[1].value = resultNode; } } } popBlock(cfg); // Create assignment nodes in the branch block for initial values pushBlockForModification(cfg, branchBlock); for (const [varName, initialValueNode] of Object.entries(this.initialVars)) { if (varName !== 'loopVar' && finalPhiNodes[varName]) { // Create an assignment statement: phiNode = initialValue const phiNodeID = finalPhiNodes[varName].id; const sourceNodeID = initialValueNode.id; // Create an assignment operation node for the initial value const assignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [phiNodeID, sourceNodeID], phiBlocks: [] }); const assignmentID = getOrCreateNode(this.strandsContext.dag, assignmentNode); recordInBasicBlock(cfg, branchBlock, assignmentID); } } popBlock(cfg); // Create assignment nodes in the final block after body execution (following conditionals pattern) // After executing the body callback, cfg.currentBlock should be the final block in the control flow pushBlockForModification(cfg, this.finalBodyBlock); for (const [varName, resultNode] of Object.entries(this.bodyResults)) { if (varName !== 'loopVar' && finalPhiNodes[varName]) { // Create an assignment statement: phiNode = bodyResult[varName] const phiNodeID = finalPhiNodes[varName].id; const sourceNodeID = resultNode.id; // Create an assignment operation node // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode) const assignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [phiNodeID, sourceNodeID], phiBlocks: [] }); const assignmentID = getOrCreateNode(this.strandsContext.dag, assignmentNode); recordInBasicBlock(cfg, this.finalBodyBlock, assignmentID); } } popBlock(cfg); // Convert phi nodes to StrandsNodes for the final result const finalBodyResults = {}; for (const [varName, phiNode] of Object.entries(finalPhiNodes)) { finalBodyResults[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext); } pushBlock(cfg, mergeBlock); return finalBodyResults; } buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) { // For bounded loops, create FOR block with three statements: init, condition, update const forBlock = createBasicBlock(cfg, BlockType.FOR); addEdge(cfg, branchBlock, forBlock); // Now add only the specific nodes we need to the FOR block pushBlock(cfg, forBlock); // 1. Init statement - assign initial value to phi node (or empty if no initializer) if (initialVar) { const initAssignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [phiNode.id, initialVar.id], phiBlocks: [] }); const initAssignmentID = getOrCreateNode(this.strandsContext.dag, initAssignmentNode); recordInBasicBlock(cfg, forBlock, initAssignmentID); } // 2. Condition statement - wrap in ExpressionStatement to force generation const conditionStatementNode = createNodeData({ nodeType: NodeType.STATEMENT, statementType: StatementType.EXPRESSION, dependsOn: [conditionNode.id], phiBlocks: [] }); const conditionStatementID = getOrCreateNode(this.strandsContext.dag, conditionStatementNode); recordInBasicBlock(cfg, forBlock, conditionStatementID); // 3. Update statement - create assignment of update result to phi node const updateAssignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [phiNode.id, updateResult.id], phiBlocks: [] }); const updateAssignmentID = getOrCreateNode(this.strandsContext.dag, updateAssignmentNode); recordInBasicBlock(cfg, forBlock, updateAssignmentID); popBlock(cfg); // Verify we have the right number of statements (2 or 3 depending on initializer) const instructions = cfg.blockInstructions[forBlock] || []; const expectedLength = initialVar ? 3 : 2; if (instructions.length !== expectedLength) { throw new Error(`FOR block must have exactly ${expectedLength} statements, got ${instructions.length}`); } const scopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START); addEdge(cfg, forBlock, scopeStartBlock); const bodyBlock = createBasicBlock(cfg, BlockType.DEFAULT); this.bodyBlock = bodyBlock; addEdge(cfg, scopeStartBlock, bodyBlock); this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode); const scopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END); addEdge(cfg, bodyBlock, scopeEndBlock); addEdge(cfg, scopeEndBlock, mergeBlock); } buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) { // For unbounded loops, create FOR block with infinite loop and break condition const forBlock = createBasicBlock(cfg, BlockType.FOR); addEdge(cfg, branchBlock, forBlock); // Create FOR block with three empty statements for for(;;) syntax pushBlock(cfg, forBlock); // 1. Init statement - initialize loop variable or empty if (initialVar) { const initAssignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [phiNode.id, initialVar.id], phiBlocks: [] }); const initAssignmentID = getOrCreateNode(this.strandsContext.dag, initAssignmentNode); recordInBasicBlock(cfg, forBlock, initAssignmentID); } else { // Create empty statement for init const emptyInitNode = createNodeData({ nodeType: NodeType.STATEMENT, statementType: StatementType.EMPTY, dependsOn: [], phiBlocks: [] }); const emptyInitID = getOrCreateNode(this.strandsContext.dag, emptyInitNode); recordInBasicBlock(cfg, forBlock, emptyInitID); } // 2. Condition statement - empty for infinite loop const emptyConditionNode = createNodeData({ nodeType: NodeType.STATEMENT, statementType: StatementType.EMPTY, dependsOn: [], phiBlocks: [] }); const emptyConditionID = getOrCreateNode(this.strandsContext.dag, emptyConditionNode); recordInBasicBlock(cfg, forBlock, emptyConditionID); // 3. Update statement - empty for infinite loop const emptyUpdateNode = createNodeData({ nodeType: NodeType.STATEMENT, statementType: StatementType.EMPTY, dependsOn: [], phiBlocks: [] }); const emptyUpdateID = getOrCreateNode(this.strandsContext.dag, emptyUpdateNode); recordInBasicBlock(cfg, forBlock, emptyUpdateID); popBlock(cfg); const scopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START); addEdge(cfg, forBlock, scopeStartBlock); // Add break condition check right after scope start const breakCheckBlock = createBasicBlock(cfg, BlockType.DEFAULT); addEdge(cfg, scopeStartBlock, breakCheckBlock); pushBlock(cfg, breakCheckBlock); // Generate break statement: if (!condition) break; // First, create the logical NOT of the condition: !condition const condition = conditionNode; const negatedCondition = this.createLogicalNotNode(condition); // Create a conditional break using the existing conditional structure // We'll create an IF_COND block that leads to a break statement const breakConditionBlock = createBasicBlock(cfg, BlockType.IF_COND); addEdge(cfg, breakCheckBlock, breakConditionBlock); cfg.blockConditions[breakConditionBlock] = negatedCondition.id; // Add scope start block for break statement const breakScopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START); addEdge(cfg, breakConditionBlock, breakScopeStartBlock); const breakStatementBlock = createBasicBlock(cfg, BlockType.DEFAULT); addEdge(cfg, breakScopeStartBlock, breakStatementBlock); // Create the break statement in the break statement block pushBlock(cfg, breakStatementBlock); const breakStatementNode = createNodeData({ nodeType: NodeType.STATEMENT, statementType: StatementType.BREAK, dependsOn: [], phiBlocks: [] }); const breakStatementID = getOrCreateNode(this.strandsContext.dag, breakStatementNode); recordInBasicBlock(cfg, breakStatementBlock, breakStatementID); popBlock(cfg); // Add scope end block for break statement const breakScopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END); addEdge(cfg, breakStatementBlock, breakScopeEndBlock); // The break scope end block leads to the merge block (exits the loop) addEdge(cfg, breakScopeEndBlock, mergeBlock); popBlock(cfg); const bodyBlock = createBasicBlock(cfg, BlockType.DEFAULT); this.bodyBlock = bodyBlock; addEdge(cfg, breakCheckBlock, bodyBlock); this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode); const updateBlock = createBasicBlock(cfg, BlockType.DEFAULT); addEdge(cfg, bodyBlock, updateBlock); // Update the loop variable in the update block (like bounded loops) pushBlock(cfg, updateBlock); const updateAssignmentNode = createNodeData({ nodeType: NodeType.ASSIGNMENT, dependsOn: [phiNode.id, updateResult.id], phiBlocks: [] }); const updateAssignmentID = getOrCreateNode(this.strandsContext.dag, updateAssignmentNode); recordInBasicBlock(cfg, updateBlock, updateAssignmentID); popBlock(cfg); const scopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END); addEdge(cfg, updateBlock, scopeEndBlock); // Connect end of for loop to the merge agter the loop addEdge(cfg, scopeEndBlock, mergeBlock); // Break condition exits to merge addEdge(cfg, breakCheckBlock, mergeBlock); } initializeLoopVariable(cfg, branchBlock) { pushBlock(cfg, branchBlock); let initialVar = this.initialCb(); // Convert to StrandsNode if it's not already one if (!(initialVar instanceof StrandsNode)) { const { id, dimension } = primitiveConstructorNode(this.strandsContext, { baseType: BaseType.FLOAT, dimension: 1 }, initialVar); initialVar = createStrandsNode(id, dimension, this.strandsContext); } // Create phi node for the loop variable in the BRANCH block const phiNode = createPhiNode(this.strandsContext, [ { value: initialVar, blockId: branchBlock }, { value: initialVar, blockId: branchBlock } // Placeholder, will be updated later ], 'loopVar'); popBlock(cfg); return { initialVar, phiNode }; } createLogicalNotNode(conditionNode) { const notOperationNode = createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Unary.LOGICAL_NOT, baseType: BaseType.BOOL, dimension: 1, dependsOn: [conditionNode.id], phiBlocks: [], usedBy: [] }); const notOperationID = getOrCreateNode(this.strandsContext.dag, notOperationNode); return createStrandsNode(notOperationID, 1, this.strandsContext); } executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode) { pushBlock(cfg, bodyBlock); // Create phi node references to pass to the body callback const phiVars = {}; const phiNodesForBody = {}; pushBlockForModification(cfg, branchBlock); for (const [varName, initialValueNode] of Object.entries(this.initialVars)) { if (varName !== 'loopVar') { // Create phi node that will be used for the final result const varPhiNode = createPhiNode(this.strandsContext, [ { value: initialValueNode, blockId: branchBlock }, // Initial value { value: initialValueNode, blockId: bodyBlock } // Placeholder - will update after body execution ], varName); phiNodesForBody[varName] = varPhiNode; phiVars[varName] = createStrandsNode(varPhiNode.id, varPhiNode.dimension, this.strandsContext); } } popBlock(cfg); const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext); this.bodyResults = this.bodyCb(loopVarNode, phiVars) || {}; for (const key in this.bodyResults) { this.bodyResults[key] = this.strandsContext.p5.strandsNode(this.bodyResults[key]); } this.phiNodesForBody = phiNodesForBody; // Capture the final block after body execution before popping this.finalBodyBlock = cfg.currentBlock; popBlock(cfg); } loopIsBounded(initialVar, conditionNode, updateVar) { // A loop is considered "bounded" if we can determine at compile time that it will // execute a known number of iterations. This happens when: // 1. The condition compares the loop variable against a compile-time constant // 2. At least one side of the comparison uses only literals (no variables/uniforms) if (!conditionNode) return false; // Analyze the condition node - it should be a comparison operation const conditionData = getNodeDataFromID(this.strandsContext.dag, conditionNode.id); if (conditionData.nodeType !== NodeType.OPERATION) { return false; } // For a comparison like "i < bound", we need at least one side to use only literals // The condition should have two dependencies: left and right operands if (!conditionData.dependsOn || conditionData.dependsOn.length !== 2) { return false; } // Check if either operand uses only literals const leftOperand = createStrandsNode(conditionData.dependsOn[0], 1, this.strandsContext); const rightOperand = createStrandsNode(conditionData.dependsOn[1], 1, this.strandsContext); const leftUsesOnlyLiterals = this.nodeUsesOnlyLiterals(leftOperand); const rightUsesOnlyLiterals = this.nodeUsesOnlyLiterals(rightOperand); // At least one side should use only literals for the loop to be bounded return leftUsesOnlyLiterals || rightUsesOnlyLiterals; } nodeUsesOnlyLiterals(node) { // Recursively check if a node and all its dependencies use only literals const nodeData = getNodeDataFromID(this.strandsContext.dag, node.id); switch (nodeData.nodeType) { case NodeType.LITERAL: return true; case NodeType.VARIABLE: // Variables (like uniforms) make this branch unbounded return false; case NodeType.PHI: // Phi nodes (like loop variables) are not literals return false; case NodeType.OPERATION: // For operations, all dependencies must use only literals if (nodeData.dependsOn) { for (const depId of nodeData.dependsOn) { const depNode = createStrandsNode(depId, 1, this.strandsContext); if (!this.nodeUsesOnlyLiterals(depNode)) { return false; } } } return true; default: // Conservative: if we don't know the node type, assume not literal return false; } } } const BUILTIN_GLOBAL_SPECS = { width: { typeInfo: DataType.float1, get: (p) => p.width }, height: { typeInfo: DataType.float1, get: (p) => p.height }, mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX }, mouseY: { typeInfo: DataType.float1, get: (p) => p.mouseY }, pmouseX: { typeInfo: DataType.float1, get: (p) => p.pmouseX }, pmouseY: { typeInfo: DataType.float1, get: (p) => p.pmouseY }, winMouseX: { typeInfo: DataType.float1, get: (p) => p.winMouseX }, winMouseY: { typeInfo: DataType.float1, get: (p) => p.winMouseY }, pwinMouseX: { typeInfo: DataType.float1, get: (p) => p.pwinMouseX }, pwinMouseY: { typeInfo: DataType.float1, get: (p) => p.pwinMouseY }, frameCount: { typeInfo: DataType.float1, get: (p) => p.frameCount }, deltaTime: { typeInfo: DataType.float1, get: (p) => p.deltaTime }, displayWidth: { typeInfo: DataType.float1, get: (p) => p.displayWidth }, displayHeight: { typeInfo: DataType.float1, get: (p) => p.displayHeight }, windowWidth: { typeInfo: DataType.float1, get: (p) => p.windowWidth }, windowHeight: { typeInfo: DataType.float1, get: (p) => p.windowHeight }, mouseIsPressed: { typeInfo: DataType.bool1, get: (p) => p.mouseIsPressed }, }; function _getBuiltinGlobalsCache(strandsContext) { if (!strandsContext._builtinGlobals || strandsContext._builtinGlobals.dag !== strandsContext.dag) { strandsContext._builtinGlobals = { dag: strandsContext.dag, nodes: new Map(), uniformsAdded: new Set(), }; } // return the cache return strandsContext._builtinGlobals } function getBuiltinGlobalNode(strandsContext, name) { const spec = BUILTIN_GLOBAL_SPECS[name]; if (!spec) return null const cache = _getBuiltinGlobalsCache(strandsContext); const uniformName = `_p5_global_${name}`; const cached = cache.nodes.get(uniformName); if (cached) return cached if (!cache.uniformsAdded.has(uniformName)) { cache.uniformsAdded.add(uniformName); strandsContext.uniforms.push({ name: uniformName, typeInfo: spec.typeInfo, defaultValue: () => { const p5Instance = strandsContext.renderer?._pInst || strandsContext.p5?.instance; return p5Instance ? spec.get(p5Instance) : undefined }, }); } const { id, dimension } = variableNode(strandsContext, spec.typeInfo, uniformName); const node = createStrandsNode(id, dimension, strandsContext); node._originalBuiltinName = name; cache.nodes.set(uniformName, node); return node } function installBuiltinGlobalAccessors(strandsContext) { if (strandsContext._builtinGlobalsAccessorsInstalled) return const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance; for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) { const spec = BUILTIN_GLOBAL_SPECS[name]; Object.defineProperty(window, name, { get: () => { if (strandsContext.active) { return getBuiltinGlobalNode(strandsContext, name); } const inst = getRuntimeP5Instance(); return spec.get(inst); }, }); } strandsContext._builtinGlobalsAccessorsInstalled = true; } ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// function initGlobalStrandsAPI(p5, fn, strandsContext) { // We augment the strands node with operations programatically // this means methods like .add, .sub, etc can be chained for (const { name, arity, opCode } of OperatorTable) { if (arity === 'binary') { StrandsNode.prototype[name] = function (...right) { const { id, dimension } = binaryOpNode(strandsContext, this, right, opCode); return createStrandsNode(id, dimension, strandsContext); }; } if (arity === 'unary') { p5[name] = function (nodeOrValue) { const { id, dimension } = unaryOpNode(strandsContext, nodeOrValue, opCode); return createStrandsNode(id, dimension, strandsContext); }; } } ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// fn.discard = function() { statementNode(strandsContext, StatementType.DISCARD); }; fn.break = function() { statementNode(strandsContext, StatementType.BREAK); }; p5.break = fn.break; fn.instanceID = function() { const node = variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, strandsContext.backend.instanceIdReference()); return createStrandsNode(node.id, node.dimension, strandsContext); }; // Internal methods use p5 static methods; user-facing methods use fn. // Some methods need to be used by both. p5.strandsIf = function(conditionNode, ifBody) { return new StrandsConditional(strandsContext, conditionNode, ifBody); }; fn.strandsIf = p5.strandsIf; p5.strandsFor = function(initialCb, conditionCb, updateCb, bodyCb, initialVars) { return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build(); }; fn.strandsFor = p5.strandsFor; p5.strandsEarlyReturn = function(value) { const { dag, cfg } = strandsContext; // Ensure we're inside a hook if (!strandsContext.activeHook) { throw new Error('strandsEarlyReturn can only be used inside a hook callback'); } // Convert value to a StrandsNode if it isn't already const valueNode = value instanceof StrandsNode ? value : p5.strandsNode(value); // Create a new CFG block for the early return const earlyReturnBlockID = createBasicBlock(cfg, BlockType.DEFAULT); addEdge(cfg, cfg.currentBlock, earlyReturnBlockID); pushBlock(cfg, earlyReturnBlockID); // Create the early return statement node const nodeData = createNodeData({ nodeType: NodeType.STATEMENT, statementType: StatementType.EARLY_RETURN, dependsOn: [valueNode.id] }); const earlyReturnID = getOrCreateNode(dag, nodeData); recordInBasicBlock(cfg, cfg.currentBlock, earlyReturnID); // Add the value to the hook's earlyReturns array for later type checking strandsContext.activeHook.earlyReturns.push({ earlyReturnID, valueNode }); popBlock(cfg); return valueNode; }; fn.strandsEarlyReturn = p5.strandsEarlyReturn; p5.strandsNode = function(...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { return args[0]; } if (args.length > 4) { userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported."); } const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, args.flat()); return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext); }; ////////////////////////////////////////////// // Builtins, uniforms, variable constructors ////////////////////////////////////////////// for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) { const isp5Function = overrides[0].isp5Function; if (isp5Function) { const originalFn = fn[functionName]; fn[functionName] = function(...args) { if (strandsContext.active) { const { id, dimension } = functionCallNode(strandsContext, functionName, args); return createStrandsNode(id, dimension, strandsContext); } else { return originalFn.apply(this, args); } }; } else { fn[functionName] = function (...args) { if (strandsContext.active) { const { id, dimension } = functionCallNode(strandsContext, functionName, args); return createStrandsNode(id, dimension, strandsContext); } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` ); } }; } } fn.getTexture = function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); return createStrandsNode(id, dimension, strandsContext); } else { p5._friendlyError( `It looks like you've called getTexture outside of a shader's modify() function.` ); } }; // Add texture function as alias for getTexture with p5 fallback const originalTexture = fn.texture; fn.texture = function (...args) { if (strandsContext.active) { return this.getTexture(...args); } else { return originalTexture.apply(this, args); } }; // Add noise function with backend-agnostic implementation const originalNoise = fn.noise; const originalNoiseDetail = fn.noiseDetail; strandsContext._noiseOctaves = null; strandsContext._noiseAmpFalloff = null; fn.noiseDetail = function (lod, falloff = 0.5) { if (!strandsContext.active) { return originalNoiseDetail.apply(this, arguments); } strandsContext._noiseOctaves = lod; strandsContext._noiseAmpFalloff = falloff; }; fn.noise = function (...args) { if (!strandsContext.active) { return originalNoise.apply(this, args); // fallback to regular p5.js noise } // Get noise shader snippet from the current renderer const noiseSnippet = this._renderer.getNoiseShaderSnippet(); strandsContext.vertexDeclarations.add(noiseSnippet); strandsContext.fragmentDeclarations.add(noiseSnippet); // Make each input into a strands node so that we can check their dimensions const strandsArgs = args.flat().map(arg => p5.strandsNode(arg)); let nodeArgs; if (strandsArgs.length === 3) { nodeArgs = [fn.vec3(strandsArgs[0], strandsArgs[1], strandsArgs[2])]; } else if (strandsArgs.length === 2) { nodeArgs = [fn.vec3(strandsArgs[0], strandsArgs[1], 0)]; } else if (strandsArgs.length === 1 && strandsArgs[0].dimension <= 3) { if (strandsArgs[0].dimension === 3) { nodeArgs = strandsArgs; } else if (strandsArgs[0].dimension === 2) { nodeArgs = [fn.vec3(strandsArgs[0], 0)]; } else { nodeArgs = [fn.vec3(strandsArgs[0], 0, 0)]; } } else { p5._friendlyError( `It looks like you've called noise() with ${args.length} arguments. It only supports 1D to 3D input.` ); } const octaves = strandsContext._noiseOctaves !== null ? strandsContext._noiseOctaves : fn._getNoiseOctaves(); const falloff = strandsContext._noiseAmpFalloff !== null ? strandsContext._noiseAmpFalloff : fn._getNoiseAmpFalloff(); nodeArgs.push(octaves); nodeArgs.push(falloff); const { id, dimension } = functionCallNode(strandsContext, 'noise', nodeArgs, { overloads: [{ params: [DataType.float3, DataType.int1, DataType.float1], returnType: DataType.float1, }] }); return createStrandsNode(id, dimension, strandsContext); }; // Next is type constructors and uniform functions. // For some of them, we have aliases so that you can write either a more human-readable // variant or also one more directly translated from GLSL, or to be more compatible with // APIs we documented at the release of 2.x and have to continue supporting. for (const type in DataType) { if (type === BaseType.DEFER || type === 'sampler') { continue; } const typeInfo = DataType[type]; const typeAliases = []; let pascalTypeName; if (/^[ib]vec/.test(typeInfo.fnName)) { pascalTypeName = typeInfo.fnName .slice(0, 2).toUpperCase() + typeInfo.fnName .slice(2) .toLowerCase(); typeAliases.push(pascalTypeName.replace('Vec', 'Vector')); } else { pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase() + typeInfo.fnName.slice(1); if (pascalTypeName === 'Sampler2D') { typeAliases.push('Texture'); } else if (/^vec/.test(typeInfo.fnName)) { typeAliases.push(pascalTypeName.replace('Vec', 'Vector')); } } fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { const { id, dimension } = variableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return createStrandsNode(id, dimension, strandsContext); }; // Shared variables with smart context detection fn[`shared${pascalTypeName}`] = function(name) { const { id, dimension } = variableNode(strandsContext, typeInfo, name); // Initialize shared variables tracking if not present if (!strandsContext.sharedVariables) { strandsContext.sharedVariables = new Map(); } // Track this shared variable for smart declaration generation strandsContext.sharedVariables.set(name, { typeInfo, usedInVertex: false, usedInFragment: false, declared: false }); return createStrandsNode(id, dimension, strandsContext); }; // Alias varying* as shared* for backward compatibility fn[`varying${pascalTypeName}`] = fn[`shared${pascalTypeName}`]; for (const typeAlias of typeAliases) { // For compatibility, also alias uniformVec2 as uniformVector2, what we initially // documented these as fn[`uniform${typeAlias}`] = fn[`uniform${pascalTypeName}`]; fn[`varying${typeAlias}`] = fn[`varying${pascalTypeName}`]; fn[`shared${typeAlias}`] = fn[`shared${pascalTypeName}`]; } const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { if (args.length === 1 && args[0].dimension && args[0].dimension === typeInfo.dimension) { const { id, dimension } = functionCallNode(strandsContext, typeInfo.fnName, args, { overloads: [{ params: [args[0].typeInfo()], returnType: typeInfo, }] }); return createStrandsNode(id, dimension, strandsContext); } else { // For vector types with a single argument, repeat it for each component if (typeInfo.dimension > 1 && args.length === 1 && !Array.isArray(args[0]) && !(args[0] instanceof StrandsNode && args[0].dimension > 1) && (typeInfo.baseType === BaseType.FLOAT || typeInfo.baseType === BaseType.INT || typeInfo.baseType === BaseType.BOOL)) { args = Array(typeInfo.dimension).fill(args[0]); } const { id, dimension } = primitiveConstructorNode(strandsContext, typeInfo, args); return createStrandsNode(id, dimension, strandsContext); } } else if (originalp5Fn) { return originalp5Fn.apply(this, args); } else { p5._friendlyError( `It looks like you've called ${typeInfo.fnName} outside of a shader's modify() function.` ); } }; } } ////////////////////////////////////////////// // Per-Hook functions ////////////////////////////////////////////// function createHookArguments(strandsContext, parameters){ const args = []; const dag = strandsContext.dag; for (const param of parameters) { if(isStructType(param.type)) { const structTypeInfo = structType(param); const { id, dimension } = structInstanceNode(strandsContext, structTypeInfo, param.name, []); const structNode = createStrandsNode(id, dimension, strandsContext).withStructProperties( structTypeInfo.properties.map(prop => prop.name) ); for (let i = 0; i < structTypeInfo.properties.length; i++) { const propertyType = structTypeInfo.properties[i]; Object.defineProperty(structNode, propertyType.name, { get() { const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]); const onRebind = (newFieldID) => { const oldDeps = dag.dependsOn[structNode.id]; const newDeps = oldDeps.slice(); newDeps[i] = newFieldID; const rebuilt = structInstanceNode(strandsContext, structTypeInfo, param.name, newDeps); structNode.id = rebuilt.id; }; // TODO: implement member access operations // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType); // const memberAccessNode = new StrandsNode(id, components); // return memberAccessNode; return createStrandsNode(propNode.id, propNode.dimension, strandsContext, onRebind); }, set(val) { const oldDependsOn = dag.dependsOn[structNode.id]; const newDependsOn = [...oldDependsOn]; let newValueID; if (val instanceof StrandsNode) { newValueID = val.id; } else { let newVal = primitiveConstructorNode(strandsContext, propertyType.dataType, val); newValueID = newVal.id; } newDependsOn[i] = newValueID; const newStructInfo = structInstanceNode(strandsContext, structTypeInfo, param.name, newDependsOn); structNode.id = newStructInfo.id; } }); } args.push(structNode); } else /*if(isNativeType(paramType.typeName))*/ { // Skip sampler parameters - they don't need strands nodes if (param.type.typeName === 'sampler') { continue; } if (!param.type.dataType) { throw new Error(`Missing dataType for parameter ${param.name} of type ${param.type.typeName}`); } const typeInfo = param.type.dataType; const { id, dimension } = variableNode(strandsContext, typeInfo, param.name); const arg = createStrandsNode(id, dimension, strandsContext); args.push(arg); } } return args; } function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { if (!(returned instanceof StrandsNode)) { // try { const result = primitiveConstructorNode(strandsContext, expectedType, returned); return result.id; // } catch (e) { // FES.userError('type error', // `There was a type mismatch for a value returned from ${hookName}.\n` + // `The value in question was supposed to be:\n` + // `${expectedType.baseType + expectedType.dimension}\n` + // `But you returned:\n` + // `${returned}` // ); // } } const dag = strandsContext.dag; let returnedNodeID = returned.id; const receivedType = { baseType: dag.baseTypes[returnedNodeID], dimension: dag.dimensions[returnedNodeID], }; if (receivedType.dimension !== expectedType.dimension) { if (receivedType.dimension !== 1) { userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookName} when a ${expectedType.baseType + expectedType.dimension} was expected!`); } else { const result = primitiveConstructorNode(strandsContext, expectedType, returned); returnedNodeID = result.id; } } else if (receivedType.baseType !== expectedType.baseType) { const result = primitiveConstructorNode(strandsContext, expectedType, returned); returnedNodeID = result.id; } return returnedNodeID; } function createShaderHooksFunctions(strandsContext, fn, shader) { installBuiltinGlobalAccessors(strandsContext); // Add shader context to hooks before spreading const vertexHooksWithContext = Object.fromEntries( Object.entries(shader.hooks.vertex).map(([name, hook]) => [name, { ...hook, shaderContext: 'vertex' }]) ); const fragmentHooksWithContext = Object.fromEntries( Object.entries(shader.hooks.fragment).map(([name, hook]) => [name, { ...hook, shaderContext: 'fragment' }]) ); const availableHooks = { ...vertexHooksWithContext, ...fragmentHooksWithContext, }; const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); const { cfg, dag } = strandsContext; for (const hookType of hookTypes) { const hook = function(hookUserCallback) { const args = setupHook(); hook._result = hookUserCallback(...args) ?? hook._result; finishHook(); }; // In the flat strands API, this is how result-returning hooks // are used hook.set = function(result) { hook._result = result; }; let entryBlockID; function setupHook() { strandsContext.activeHook = hook; entryBlockID = createBasicBlock(cfg, BlockType.FUNCTION); addEdge(cfg, cfg.currentBlock, entryBlockID); pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); const numStructArgs = hookType.parameters.filter(param => param.type.properties).length; let argIdx = -1; if (numStructArgs === 1) { argIdx = hookType.parameters.findIndex(param => param.type.properties); } for (let i = 0; i < args.length; i++) { if (i === argIdx) { for (const key of args[argIdx].structProperties || []) { Object.defineProperty(hook, key, { get() { return args[argIdx][key]; }, set(val) { args[argIdx][key] = val; }, enumerable: true, }); } if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) { hook.set(args[argIdx]); } } else { hook[hookType.parameters[i].name] = args[i]; } } return args; } function finishHook() { const userReturned = hook._result; strandsContext.activeHook = undefined; const expectedReturnType = hookType.returnType; let rootNodeID = null; const handleRetVal = (retNode) => { if(isStructType(expectedReturnType)) { const expectedStructType = structType(expectedReturnType); if (retNode instanceof StrandsNode) { const returnedNode = getNodeDataFromID(strandsContext.dag, retNode.id); if (returnedNode.baseType !== expectedStructType.typeName) { userError("type error", `You have returned a ${retNode.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`); } const newDeps = returnedNode.dependsOn.slice(); for (let i = 0; i < expectedStructType.properties.length; i++) { const expectedType = expectedStructType.properties[i].dataType; const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[retNode.id], strandsContext); newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name); } dag.dependsOn[retNode.id] = newDeps; return retNode.id; } else { const expectedProperties = expectedStructType.properties; const newStructDependencies = []; for (let i = 0; i < expectedProperties.length; i++) { const expectedProp = expectedProperties[i]; const propName = expectedProp.name; const receivedValue = retNode[propName]; if (receivedValue === undefined) { userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + `Received: { ${Object.keys(retNode).join(', ')} }\n` + `All of the properties are required!`); } const expectedTypeInfo = expectedProp.dataType; const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); newStructDependencies.push(returnedPropID); } const newStruct = structConstructorNode(strandsContext, expectedStructType, newStructDependencies); return newStruct.id; } } else /*if(isNativeType(expectedReturnType.typeName))*/ { if (!expectedReturnType.dataType) { throw new Error(`Missing dataType for return type ${expectedReturnType.typeName}`); } const expectedTypeInfo = expectedReturnType.dataType; return enforceReturnTypeMatch(strandsContext, expectedTypeInfo, retNode, hookType.name); } }; for (const { valueNode, earlyReturnID } of hook.earlyReturns) { const id = handleRetVal(valueNode); dag.dependsOn[earlyReturnID] = [id]; } rootNodeID = userReturned ? handleRetVal(userReturned) : undefined; const fullHookName = `${hookType.returnType.typeName} ${hookType.name}`; const hookInfo = availableHooks[fullHookName]; strandsContext.hooks.push({ hookType, entryBlockID, rootNodeID, shaderContext: hookInfo?.shaderContext, // 'vertex' or 'fragment' }); popBlock(cfg); } hook.begin = setupHook; hook.end = finishHook; const aliases = [hookType.name]; if (strandsContext.baseShader?.hooks?.hookAliases?.[hookType.name]) { aliases.push(...strandsContext.baseShader.hooks.hookAliases[hookType.name]); } // If the hook has a name like getPixelInputs, create an alias without // the get* prefix, like pixelInputs const nameMatch = /^get([A-Z0-9]\w*)$/.exec(hookType.name); if (nameMatch) { const unprefixedName = nameMatch[1][0].toLowerCase() + nameMatch[1].slice(1); if (!fn[unprefixedName]) { aliases.push(unprefixedName); } } for (const name of aliases) { strandsContext.windowOverrides[name] = window[name]; strandsContext.fnOverrides[name] = fn[name]; window[name] = hook; fn[name] = hook; } hook.earlyReturns = []; } } /** * @module 3D * @submodule p5.strands * @for p5 * @requires core */ function strands(p5, fn) { // Whether or not strands callbacks should be forced to be executed in global mode. // This is turned on while loading shaders from files, when there is not a feasible // way to pass context in. fn._runStrandsInGlobalMode = false; ////////////////////////////////////////////// // Global Runtime ////////////////////////////////////////////// function initStrandsContext( ctx, backend, { active = false, renderer = null, baseShader = null } = {}, ) { ctx.dag = createDirectedAcyclicGraph(); ctx.cfg = createControlFlowGraph(); ctx.uniforms = []; ctx.vertexDeclarations = new Set(); ctx.fragmentDeclarations = new Set(); ctx.hooks = []; ctx.globalAssignments = []; ctx.backend = backend; ctx.active = active; ctx.renderer = renderer; ctx.baseShader = baseShader; ctx.previousFES = p5.disableFriendlyErrors; ctx.windowOverrides = {}; ctx.fnOverrides = {}; if (active) { p5.disableFriendlyErrors = true; } ctx.p5 = p5; } function deinitStrandsContext(ctx) { ctx.dag = createDirectedAcyclicGraph(); ctx.cfg = createControlFlowGraph(); ctx.uniforms = []; ctx.vertexDeclarations = new Set(); ctx.fragmentDeclarations = new Set(); ctx.hooks = []; ctx.globalAssignments = []; ctx.active = false; p5.disableFriendlyErrors = ctx.previousFES; for (const key in ctx.windowOverrides) { window[key] = ctx.windowOverrides[key]; } for (const key in ctx.fnOverrides) { fn[key] = ctx.fnOverrides[key]; } } const strandsContext = {}; initStrandsContext(strandsContext); initGlobalStrandsAPI(p5, fn, strandsContext); function withTempGlobalMode(pInst, callback) { if (pInst._isGlobal) return callback(); const prev = {}; for (const key of Object.getOwnPropertyNames(fn)) { const descriptor = Object.getOwnPropertyDescriptor(fn, key); if (descriptor && !descriptor.get && typeof fn[key] === "function") { prev[key] = window[key]; window[key] = fn[key].bind(pInst); } } try { callback(); } finally { for (const key in prev) { window[key] = prev[key]; } } } ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// const oldModify = p5.Shader.prototype.modify; p5.Shader.prototype.modify = function (shaderModifier, scope = {}) { try { if ( shaderModifier instanceof Function || typeof shaderModifier === "string" ) { // Reset the context object every time modify is called; // const backend = glslBackend; initStrandsContext(strandsContext, this._renderer.strandsBackend, { active: true, renderer: this._renderer, baseShader: this, }); createShaderHooksFunctions(strandsContext, fn, this); // TODO: expose this, is internal for debugging for now. const options = { srcLocations: false }; // 1. Transpile from strands DSL to JS let strandsCallback; { // #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing // https://github.com/acornjs/acorn/issues/1385 const sourceString = typeof shaderModifier === "string" ? `(${shaderModifier})` : `(${shaderModifier.toString()})`; strandsCallback = transpileStrandsToJS( p5, sourceString, options.srcLocations, scope, ); } // 2. Build the IR from JavaScript API const globalScope = createBasicBlock( strandsContext.cfg, BlockType.GLOBAL, ); pushBlock(strandsContext.cfg, globalScope); if (strandsContext.renderer?._pInst?._runStrandsInGlobalMode) { withTempGlobalMode(strandsContext.renderer._pInst, strandsCallback); } else { strandsCallback(); } popBlock(strandsContext.cfg); // 3. Generate shader code hooks object from the IR // ....... const hooksObject = generateShaderCode(strandsContext); // Call modify with the generated hooks object return oldModify.call(this, hooksObject); } else { return oldModify.call(this, shaderModifier); } } finally { // Reset the strands runtime context deinitStrandsContext(strandsContext); } }; } if (typeof p5 !== "undefined") { p5.registerAddon(strands); } /* ------------------------------------------------------------- */ /** * @property {Object} worldInputs * @description * A shader hook block that modifies the world-space properties of each vertex in a shader. This hook can be used inside `buildColorShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. * * `worldInputs` has the following properties: * - `position`: a three-component vector representing the original position of the vertex. * - `normal`: a three-component vector representing the direction the surface is facing. * - `texCoord`: a two-component vector representing the texture coordinates. * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). * * This hook is available in: * - `buildMaterialShader()` * - `buildNormalShader()` * - `buildColorShader()` * - `buildStrokeShader()` * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * let t = uniformFloat(); * worldInputs.begin(); * // Move the vertex up and down in a wave in world space * // In world space, moving the object (e.g., with translate()) will affect these coordinates * // The sphere is ~50 units tall here, so 20 gives a noticeable wave * worldInputs.position.y += 20 * sin(t * 0.001 + worldInputs.position.x * 0.05); * worldInputs.end(); * } * * function draw() { * background(255); * shader(myShader); * myShader.setUniform('t', millis()); * lights(); * noStroke(); * fill('red'); * sphere(50); * } */ /** * @property {Object} combineColors * @description * A shader hook block that modifies how color components are combined in the fragment shader. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to control the final color output of a material. Modifications happen between the `.begin()` and `.end()` methods of the hook. * * `combineColors` has the following properties: * * - `baseColor`: a three-component vector representing the base color (red, green, blue). * - `diffuse`: a single number representing the diffuse reflection. * - `ambientColor`: a three-component vector representing the ambient color. * - `ambient`: a single number representing the ambient reflection. * - `specularColor`: a three-component vector representing the specular color. * - `specular`: a single number representing the specular reflection. * - `emissive`: a three-component vector representing the emissive color. * - `opacity`: a single number representing the opacity. * * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) for the final color. * * This hook is available in: * - `buildMaterialShader()` * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * combineColors.begin(); * // Custom color combination: add a green tint using vector properties * combineColors.set([ * combineColors.baseColor * combineColors.diffuse + * combineColors.ambientColor * combineColors.ambient + * combineColors.specularColor * combineColors.specular + * combineColors.emissive + * [0, 0.2, 0], // Green tint * combineColors.opacity * ]); * combineColors.end(); * } * * function draw() { * background(255); * shader(myShader); * lights(); * noStroke(); * fill('white'); * sphere(50); * } */ /** * @method smoothstep * @description * A shader function that performs smooth Hermite interpolation between `0.0` * and `1.0`. * * This function is equivalent to the GLSL built-in * `smoothstep(edge0, edge1, x)` and is available inside p5.strands shader * callbacks. It is commonly used to create soft transitions, smooth edges, * fades, and anti-aliased effects. * * Smoothstep is useful when a threshold or cutoff is needed, but with a * gradual transition instead of a hard edge. * * - Returns `0.0` when `x` is less than or equal to `edge0` * - Returns `1.0` when `x` is greater than or equal to `edge1` * - Smoothly interpolates between `0.0` and `1.0` when `x` is between them * * @param {Number} edge0 * Lower edge of the transition * @param {Number} edge1 * Upper edge of the transition * @param {Number} x * Input value to interpolate * * @returns {Number} * A value between `0.0` and `1.0` * * @example *
        * * // Example 1: A soft vertical fade using smoothstep (no uniforms) * * let fadeShader; * * function fadeCallback() { * getColor((inputs) => { * // x goes from 0 → 1 across the canvas * let x = inputs.texCoord.x; * * // smoothstep creates a soft transition instead of a hard edge * let t = smoothstep(0.25, 0.35, x); * * // Use t directly as brightness * return [t, t, t, 1]; * }); * } * * function setup() { * createCanvas(300, 200, WEBGL); * fadeShader = baseFilterShader().modify(fadeCallback); * } * * function draw() { * background(0); * filter(fadeShader); * } * *
        * * @example *
        * * // Example 2: Animate the smooth transition using a uniform * * let animatedShader; * * function animatedFadeCallback() { * const time = uniformFloat(() => millis() * 0.001); * * getColor((inputs) => { * let x = inputs.texCoord.x; * * // Move the smoothstep band back and forth over time * let center = 0.5 + 0.25 * sin(time); * let t = smoothstep(center - 0.05, center + 0.05, x); * * return [t, t, t, 1]; * }); * } * * function setup() { * createCanvas(300, 200, WEBGL); * animatedShader = baseFilterShader().modify(animatedFadeCallback); * } * * function draw() { * background(0); * filter(animatedShader); * } * *
        */ /** * @method beforeVertex * @private * @description * Registers a callback to run custom code at the very start of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every vertex before processing begins. The callback receives no arguments. * * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. * * This hook is available in: * - baseColorShader() * - baseMaterialShader() * - baseNormalShader() * - baseStrokeShader() * * @param {Function} callback * A callback function which is called before each vertex is processed. */ /** * @method afterVertex * @private * @description * Registers a callback to run custom code at the very end of the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final calculations after all vertex processing is done. The callback receives no arguments. * * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported. * * This hook is available in: * - baseColorShader() * - baseMaterialShader() * - baseNormalShader() * - baseStrokeShader() * * @param {Function} callback * A callback function which is called after each vertex is processed. */ /** * @method beforeFragment * @private * @description * Registers a callback to run custom code at the very start of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to set up variables or perform calculations that affect every pixel before color calculations begin. The callback receives no arguments. * * This hook is available in: * - baseColorShader() * - baseMaterialShader() * - baseNormalShader() * - baseStrokeShader() * * @param {Function} callback * A callback function which is called before each fragment is processed. * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseColorShader().modify(() => { * beforeFragment(() => { * // Set a value for use in getFinalColor * this.brightness = 0.5 + 0.5 * sin(millis() * 0.001); * }); * getFinalColor(color => { * // Use the value set in beforeFragment to tint the color * color.r *= this.brightness; // Tint red channel * return color; * }); * }); * } * * function draw() { * background(220); * shader(myShader); * noStroke(); * fill('teal'); * box(100); * } */ /** * @property {Object} pixelInputs * @description * A shader hook block that modifies the properties of each pixel before the final color is calculated. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust per-pixel data before lighting is applied. Modifications happen between the `.begin()` and `.end()` methods of the hook. * * The properties of `pixelInputs` depend on the shader: * * - In `buildMaterialShader()`: * - `normal`: a three-component vector representing the surface normal. * - `texCoord`: a two-component vector representing the texture coordinates (u, v). * - `ambientLight`: a three-component vector representing the ambient light color. * - `ambientMaterial`: a three-component vector representing the material's ambient color. * - `specularMaterial`: a three-component vector representing the material's specular color. * - `emissiveMaterial`: a three-component vector representing the material's emissive color. * - `color`: a four-component vector representing the base color (red, green, blue, alpha). * - `shininess`: a number controlling specular highlights. * - `metalness`: a number controlling the metalness factor. * * - In `buildStrokeShader()`: * - `color`: a four-component vector representing the stroke color (red, green, blue, alpha). * - `tangent`: a two-component vector representing the stroke tangent. * - `center`: a two-component vector representing the cap/join center. * - `position`: a two-component vector representing the current fragment position. * - `strokeWeight`: a number representing the stroke weight in pixels. * * This hook is available in: * - `buildMaterialShader()` * - `buildStrokeShader()` * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * let t = uniformFloat(); * pixelInputs.begin(); * // Animate alpha (transparency) based on x position * pixelInputs.color.a = 0.5 + 0.5 * * sin(pixelInputs.texCoord.x * 10.0 + t * 0.002); * pixelInputs.end(); * } * * function draw() { * background(240); * shader(myShader); * myShader.setUniform('t', millis()); * lights(); * noStroke(); * fill('purple'); * circle(0, 0, 100); * } */ /** * @method shouldDiscard * @private * @description * Registers a callback to decide whether to discard (skip drawing) a fragment (pixel) in the fragment shader. This hook can be used inside baseStrokeShader().modify() and similar shader modify() calls to create effects like round points or custom masking. The callback receives a boolean: * - `willDiscard`: true if the fragment would be discarded by default * * Return true to discard the fragment, or false to keep it. * * This hook is available in: * - baseStrokeShader() * * @param {Function} callback * A callback function which receives a boolean and should return a boolean. * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseStrokeShader().modify({ * 'bool shouldDiscard': '(bool outside) { return outside; }' * }); * } * * function draw() { * background(255); * strokeShader(myShader); * strokeWeight(30); * line(-width/3, 0, width/3, 0); * } */ /** * @property finalColor * @description * A shader hook block that modifies the final color of each pixel after all lighting is applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust the color before it appears on the screen. Modifications happen between the `.begin()` and `.end()` methods of the hook. * * `finalColor` has the following properties: * - `color`: a four-component vector representing the pixel color (red, green, blue, alpha). * * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color. * * This hook is available in: * - `buildColorShader()` * - `buildMaterialShader()` * - `buildNormalShader()` * - `buildStrokeShader()` * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * finalColor.begin(); * let color = finalColor.color; * // Add a blue tint to the output color * color.b += 0.4; * finalColor.set(color); * finalColor.end(); * } * * function draw() { * background(230); * shader(myShader); * noStroke(); * fill('green'); * circle(0, 0, 100); * } */ /** * @method afterFragment * @private * @description * Registers a callback to run custom code at the very end of the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to perform cleanup or final per-pixel effects after all color calculations are done. The callback receives no arguments. * * This hook is available in: * - baseColorShader() * - baseMaterialShader() * - baseNormalShader() * - baseStrokeShader() * * @param {Function} callback * A callback function which is called after each fragment is processed. * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseColorShader().modify(() => { * getFinalColor(color => { * // Add a purple tint to the color * color.b += 0.2; * return color; * }); * afterFragment(() => { * // This hook runs after the final color is set for each fragment. * // You could use this for debugging or advanced effects. * }); * }); * } * * function draw() { * background(240); * shader(myShader); * noStroke(); * fill('purple'); * sphere(60); * } */ /** * @property {Object} filterColor * @description * A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside `buildFilterShader()` to control the output color for each pixel. * * `filterColor` has the following properties: * - `texCoord`: a two-component vector representing the texture coordinates (u, v). * - `canvasSize`: a two-component vector representing the canvas size in pixels (width, height). * - `texelSize`: a two-component vector representing the size of a single texel in texture space. * - `canvasContent`: a texture containing the sketch's contents before the filter is applied. * * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color. * * This hook is available in: * - `buildFilterShader()` * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildFilterShader(warp); * } * * function warp() { * filterColor.begin(); * // Warp the texture coordinates for a wavy effect * let warped = [ * filterColor.texCoord.x, * filterColor.texCoord.y + 0.1 * sin(filterColor.texCoord.x * 10) * ]; * filterColor.set(getTexture(canvasContent, warped)); * filterColor.end(); * } * * function draw() { * background(180); * // Draw something to the canvas * fill('yellow'); * circle(0, 0, 150); * filter(myShader); * } */ /** * @property {Object} objectInputs * @description * A shader hook block to modify the properties of each vertex before any transformations are applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "Object space" refers to the coordinate system of the 3D scene before any transformations, cameras, or projection transformations are applied. * * `objectInputs` has the following properties: * - `position`: a three-component vector representing the original position of the vertex. * - `normal`: a three-component vector representing the direction the surface is facing. * - `texCoord`: a two-component vector representing the texture coordinates. * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). * * This hook is available in: * - `buildColorShader()` * - `buildMaterialShader()` * - `buildNormalShader()` * - `buildStrokeShader()` * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * let t = uniformFloat(); * objectInputs.begin(); * // Create a sine wave along the object * objectInputs.position.y += sin(t * 0.001 + objectInputs.position.x); * objectInputs.end(); * } * * function draw() { * background(220); * shader(myShader); * myShader.setUniform('t', millis()); * noStroke(); * fill('orange'); * sphere(50); * } */ /** * @property {Object} cameraInputs * @description * A shader hook block that adjusts vertex properties from the perspective of the camera. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "Camera space" refers to the coordinate system of the 3D scene after transformations have been applied, seen relative to the camera. * * `cameraInputs` has the following properties: * - `position`: a three-component vector representing the position after camera transformation. * - `normal`: a three-component vector representing the normal after camera transformation. * - `texCoord`: a two-component vector representing the texture coordinates. * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). * * This hook is available in: * - `buildColorShader()` * - `buildMaterialShader()` * - `buildNormalShader()` * - `buildStrokeShader()` * * @example * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); * myShader = buildMaterialShader(material); * } * * function material() { * let t = uniformFloat(); * cameraInputs.begin(); * // Move vertices in camera space based on their x position * cameraInputs.position.y += 30 * sin(cameraInputs.position.x * 0.05 + t * 0.001); * // Tint all vertices blue * cameraInputs.color.b = 1; * cameraInputs.end(); * } * * function draw() { * background(200); * shader(myShader); * myShader.setUniform('t', millis()); * noStroke(); * fill('red'); * sphere(50); * } */ /** * Retrieves the current color of a given texture at given coordinates. * * The given coordinates should be between [0, 0] representing the top-left of * the texture, and [1, 1] representing the bottom-right of the texture. * * The given texture could be, for example: * * p5.Image, * * a p5.Graphics, or * * a p5.Framebuffer. * * The retrieved color that is returned will behave like a vec4, with components * for red, green, blue, and alpha, each between 0.0 and 1.0. * * Linear interpolation is used by default. For Framebuffer sources, you can * prevent this by creating the buffer with: * ```js * createFramebuffer({ * textureFiltering: NEAREST * }) * ``` * This can be useful if you are using your texture to store data other than color. * See createFramebuffer. * * Note: The `getTexture` function is only available when using p5.strands. * * @method getTexture * @beta * * @param texture The texture to sample from. * (e.g. a p5.Image, p5.Graphics, or p5.Framebuffer). * * @param coords The 2D coordinates to sample from. * This should be between [0,0] (the top-left) and [1,1] (the bottom-right) * of the texture. It should be compatible with a vec2. * * @returns {*} The color of the given texture at the given coordinates. This * will behave as a vec4 holding components r, g, b, and a (alpha), with each component being in the range 0.0 to 1.0. * * @example *
        * * // A filter shader (using p5.strands) which will * // sample and invert the color of each pixel * // from the canvas. * function setup() { * createCanvas(100, 100, WEBGL); * let myShader = buildFilterShader(buildIt); * * background("white"); * fill("red"); * circle(0, 0, 50); * * filter(myShader); //Try commenting this out! * * describe("A cyan circle on black background"); * } * * function buildIt() { * filterColor.begin(); * * //Sample the color of the pixel from the * //canvas at the same coordinate. * let c = getTexture(filterColor.canvasContent, * filterColor.texCoord); * * //Make a new color by inverting r, g, and b * let newColor = [1 - c.r, 1 - c.g, 1 - c.b, c.a]; * * //Finally, use it for this pixel! * filterColor.set(newColor); * * filterColor.end(); * } * * * * @example *
        * * // This primitive edge-detection filter samples * // and compares the colors of the current pixel * // on the canvas, and a little to the right. * // It marks if they differ much. * let myShader; * * function setup() { * createCanvas(100, 100, WEBGL); * myShader = buildFilterShader(myShaderBuilder); * describe("A rough partial outline of a square rotating around a circle"); * } * * function draw() { * drawADesign(); * * filter(myShader); // try commenting this out * } * * function myShaderBuilder() { * filterColor.begin(); * * //The position of the current pixel... * let coordHere = filterColor.texCoord; * //and some small amount to the right. * let coordRight = coordHere + [0.01, 0]; * * //The canvas content is a texture. * let cnvTex = filterColor.canvasContent; * * //Sample the colors from it at our two positions * let colorHere = getTexture(cnvTex, coordHere); * let colorRight = getTexture(cnvTex, coordRight); * * // Calculate a (very rough) color difference. * let difference = length(colorHere - colorRight); * * //We'll use a black color by default... * let resultColor = [0, 0, 0, 1]; * //or white if the samples were different. * if (difference > 0.3) { * resultColor = [1, 1, 1, 1]; * } * filterColor.set(resultColor); * * filterColor.end(); * } * * //Draw a few shapes, just to test the filter with * function drawADesign() { * background(50); * noStroke(); * lights(); * sphere(20); * rotate(frameCount / 300); * square(0, 0, 30); * } * *
        */ /** * @method getWorldInputs * @param {Function} callback */ /** * @method getPixelInputs * @param {Function} callback */ /** * @method getFinalColor * @param {Function} callback */ /** * @method getColor * @param {Function} callback */ /** * @method getObjectInputs * @param {Function} callback */ /** * @method getCameraInputs * @param {Function} callback */ /** * This file setup global mode automatic instantiation * * if sketch is on window * assume "global" mode * and instantiate p5 automatically * otherwise do nothing * * @private * @return {Undefined} */ const _globalInit = () => { // Could have been any property defined within the p5 constructor. // If that property is already a part of the global object, // this code has already run before, likely due to a duplicate import if (typeof window._setupDone !== 'undefined') { console.warn( 'p5.js seems to have been imported multiple times. Please remove the duplicate import' ); return; } if (!window.mocha) { const p5ReadyEvent = new Event('p5Ready'); window.dispatchEvent(p5ReadyEvent); // If there is a setup or draw function on the window // then instantiate p5 in "global" mode if ( ((window.setup && typeof window.setup === 'function') || (window.draw && typeof window.draw === 'function')) && !p5$2.instance ) { new p5$2(); } } }; // make a promise that resolves when the document is ready const waitForDocumentReady = () => new Promise((resolve, reject) => { // if the page is ready, initialize p5 immediately if (document.readyState === 'complete') { resolve(); // if the page is still loading, add an event listener // and initialize p5 as soon as it finishes loading } else { window.addEventListener('load', resolve, false); } }); // only load translations if we're using the full, un-minified library const waitingForTranslator = typeof IS_MINIFIED === 'undefined' ? initialize() : Promise.resolve(); // core shape(p5$2); accessibility(p5$2); color(p5$2); friendlyErrors(p5$2); data(p5$2); dom(p5$2); events(p5$2); image(p5$2); io(p5$2); math(p5$2); utilities(p5$2); webgl(p5$2); type(p5$2); p5$2.registerAddon(shader); p5$2.registerAddon(strands); Promise.all([waitForDocumentReady(), waitingForTranslator]).then(_globalInit); return p5$2; })();