diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index bd3ca55ba0..82216dcc4a 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -27,6 +27,14 @@ jobs: run: npm test env: CI: true + - name: generate TypeScript types + run: npm run generate-types + env: + CI: true + - name: test TypeScript types + run: npm run test:types + env: + CI: true - name: report test coverage run: bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json env: diff --git a/docs/parameterData.json b/docs/parameterData.json index d9747b17a5..12241d5307 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -31,6 +31,11 @@ ] ] }, + "remove": { + "overloads": [ + [] + ] + }, "p5": { "overloads": [ [ @@ -206,7 +211,8 @@ "Number", "Number", "Number?" - ] + ], + [] ] }, "fill": { @@ -475,7 +481,7 @@ "applyMatrix": { "overloads": [ [ - "Array" + "Number[]" ], [ "Number", @@ -765,6 +771,31 @@ ] ] }, + "createVideo": { + "overloads": [ + [ + "String|String[]?", + "Function?" + ] + ] + }, + "createAudio": { + "overloads": [ + [ + "String|String[]?", + "Function?" + ] + ] + }, + "createCapture": { + "overloads": [ + [ + "AUDIO|VIDEO|Object?", + "Object?", + "Function?" + ] + ] + }, "setMoveThreshold": { "overloads": [ [ @@ -1262,6 +1293,13 @@ ] ] }, + "setContent": { + "overloads": [ + [ + "String" + ] + ] + }, "abs": { "overloads": [ [ @@ -1300,9 +1338,6 @@ "Number", "Number", "Number" - ], - [ - "p5.Vector" ] ] }, @@ -1985,6 +2020,55 @@ ] ] }, + "getWorldInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, + "combineColors": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getPixelInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getFinalColor": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getColor": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getObjectInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getCameraInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, "loadFont": { "overloads": [ [ @@ -2015,10 +2099,9 @@ "textAlign": { "overloads": [ [ - "LEFT|CENTER|RIGHT", + "LEFT|CENTER|RIGHT?", "TOP|BOTTOM|CENTER|BASELINE?" - ], - [] + ] ] }, "textAscent": { @@ -2038,18 +2121,16 @@ "textLeading": { "overloads": [ [ - "Number" - ], - [] + "Number?" + ] ] }, "textFont": { "overloads": [ [ - "p5.Font|String|Object", + "p5.Font|String|Object?", "Number?" - ], - [] + ] ] }, "textSize": { @@ -2605,7 +2686,7 @@ "imageLight": { "overloads": [ [ - "p5.image" + "p5.Image" ] ] }, @@ -2724,7 +2805,7 @@ [ "String|Request", "String?", - "Boolean", + "Boolean?", "function(p5.Geometry)?", "function(Event)?" ], @@ -2753,7 +2834,7 @@ [ "String", "String?", - "Boolean", + "Boolean?", "function(p5.Geometry)?", "function(Event)?" ], @@ -2945,6 +3026,13 @@ ] ] }, + "roll": { + "overloads": [ + [ + "Number" + ] + ] + }, "camera": { "overloads": [ [ @@ -3029,99 +3117,38 @@ ] ] }, - "setAttributes": { - "overloads": [ - [ - "String", - "Boolean" - ], - [ - "Object" - ] - ] - }, - "remove": { - "overloads": [ - [] - ] - }, - "createVideo": { + "fromAxisAngle": { "overloads": [ [ - "String|String[]", - "Function?" + "Number?", + "Number?", + "Number?", + "Number?" ] ] }, - "createAudio": { + "mult": { "overloads": [ - [], [ - "String|String[]?", - "Function?" + "p5.Quat?" ] ] }, - "createCapture": { + "rotateBy": { "overloads": [ [ - "AUDIO|VIDEO|Object?", - "Object?", - "Function?" + "p5.Quat?" ] ] - } - }, - "p5.Geometry": { - "flipV": { - "overloads": [ - [] - ] - }, - "calculateBoundingBox": { - "overloads": [ - [] - ] }, - "clearColors": { - "overloads": [ - [] - ] - }, - "flipU": { - "overloads": [ - [] - ] - }, - "computeFaces": { - "overloads": [ - [] - ] - }, - "computeNormals": { - "overloads": [ - [ - "FLAT|SMOOTH?", - "Object?" - ] - ] - }, - "makeEdgesFromFaces": { - "overloads": [ - [] - ] - }, - "normalize": { - "overloads": [ - [] - ] - }, - "vertexProperty": { + "setAttributes": { "overloads": [ [ "String", - "Number|Number[]", - "Number?" + "Boolean" + ], + [ + "Object" ] ] } @@ -3479,9 +3506,8 @@ }, "time": { "overloads": [ - [], [ - "Number" + "Number?" ] ] }, @@ -3557,10 +3583,10 @@ "updatePixels": { "overloads": [ [ - "Integer", - "Integer", - "Integer", - "Integer" + "Integer?", + "Integer?", + "Integer?", + "Integer?" ] ] }, @@ -4073,10 +4099,7 @@ }, "copy": { "overloads": [ - [], - [ - "p5.Vector" - ] + [] ] }, "add": { @@ -4088,11 +4111,6 @@ ], [ "p5.Vector|Number[]" - ], - [ - "p5.Vector", - "p5.Vector", - "p5.Vector?" ] ] }, @@ -4105,10 +4123,6 @@ ], [ "p5.Vector|Number[]" - ], - [ - "p5.Vector", - "p5.Vector" ] ] }, @@ -4121,16 +4135,14 @@ ], [ "p5.Vector|Number[]" - ], - [ - "p5.Vector", - "p5.Vector", - "p5.Vector?" ] ] }, "mult": { "overloads": [ + [ + "Number" + ], [ "Number", "Number", @@ -4141,22 +4153,6 @@ ], [ "p5.Vector" - ], - [], - [ - "p5.Vector", - "Number", - "p5.Vector?" - ], - [ - "p5.Vector", - "p5.Vector", - "p5.Vector?" - ], - [ - "p5.Vector", - "Number[]", - "p5.Vector?" ] ] }, @@ -4175,39 +4171,17 @@ ], [ "p5.Vector" - ], - [], - [ - "p5.Vector", - "Number", - "p5.Vector?" - ], - [ - "p5.Vector", - "p5.Vector", - "p5.Vector?" - ], - [ - "p5.Vector", - "Number[]", - "p5.Vector?" ] ] }, "mag": { "overloads": [ - [], - [ - "p5.Vector" - ] + [] ] }, "magSq": { "overloads": [ - [], - [ - "p5.Vector" - ] + [] ] }, "dot": { @@ -4219,11 +4193,6 @@ ], [ "p5.Vector" - ], - [], - [ - "p5.Vector", - "p5.Vector" ] ] }, @@ -4231,33 +4200,25 @@ "overloads": [ [ "p5.Vector" - ], - [], + ] + ] + }, + "dist": { + "overloads": [ [ - "p5.Vector", "p5.Vector" ] ] }, "normalize": { "overloads": [ - [], - [ - "p5.Vector", - "p5.Vector?" - ] + [] ] }, "limit": { "overloads": [ [ "Number" - ], - [], - [ - "p5.Vector", - "Number", - "p5.Vector?" ] ] }, @@ -4265,21 +4226,12 @@ "overloads": [ [ "Number" - ], - [], - [ - "p5.Vector", - "Number", - "p5.Vector?" ] ] }, "heading": { "overloads": [ - [], - [ - "p5.Vector" - ] + [] ] }, "setHeading": { @@ -4293,12 +4245,6 @@ "overloads": [ [ "Number" - ], - [], - [ - "p5.Vector", - "Number", - "p5.Vector?" ] ] }, @@ -4306,11 +4252,6 @@ "overloads": [ [ "p5.Vector" - ], - [], - [ - "p5.Vector", - "p5.Vector" ] ] }, @@ -4325,13 +4266,6 @@ [ "p5.Vector", "Number" - ], - [], - [ - "p5.Vector", - "p5.Vector", - "Number", - "p5.Vector?" ] ] }, @@ -4340,13 +4274,6 @@ [ "p5.Vector", "Number" - ], - [], - [ - "p5.Vector", - "p5.Vector", - "Number", - "p5.Vector?" ] ] }, @@ -4354,21 +4281,12 @@ "overloads": [ [ "p5.Vector" - ], - [], - [ - "p5.Vector", - "p5.Vector", - "p5.Vector?" ] ] }, "array": { "overloads": [ - [], - [ - "p5.Vector" - ] + [] ] }, "equals": { @@ -4380,14 +4298,14 @@ ], [ "p5.Vector|Array" - ], - [], - [ - "p5.Vector|Array", - "p5.Vector|Array" ] ] }, + "clampToZero": { + "overloads": [ + [] + ] + }, "fromAngle": { "overloads": [ [ @@ -4414,15 +4332,6 @@ "overloads": [ [] ] - }, - "dist": { - "overloads": [ - [], - [ - "p5.Vector", - "p5.Vector" - ] - ] } }, "p5.Font": { @@ -4433,8 +4342,7 @@ "Number", "Number", "Number?", - "Number?", - "Object?" + "Number?" ] ] }, @@ -4629,6 +4537,11 @@ ] ] }, + "loadPixels": { + "overloads": [ + [] + ] + }, "get": { "overloads": [ [ @@ -4645,6 +4558,60 @@ ] } }, + "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": [ @@ -4658,6 +4625,10 @@ }, "modify": { "overloads": [ + [ + "Function", + "Object?" + ], [ "Object?" ] diff --git a/package-lock.json b/package-lock.json index a4cea33377..c1c7d53960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "rollup": "^4.9.6", "rollup-plugin-string": "^3.0.0", "rollup-plugin-visualizer": "^5.12.0", + "typescript": "^5.9.2", "vite": "^5.0.2", "vite-plugin-string": "^1.2.2", "vitest": "^2.1.5", @@ -3144,14 +3145,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", - "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.1.tgz", + "integrity": "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.0", - "@typescript-eslint/types": "^8.34.0", + "@typescript-eslint/tsconfig-utils": "^8.44.1", + "@typescript-eslint/types": "^8.44.1", "debug": "^4.3.4" }, "engines": { @@ -3162,18 +3163,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", - "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz", + "integrity": "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0" + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3184,9 +3185,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", - "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz", + "integrity": "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==", "dev": true, "license": "MIT", "engines": { @@ -3197,13 +3198,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", - "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.1.tgz", + "integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==", "dev": true, "license": "MIT", "engines": { @@ -3215,16 +3216,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", - "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz", + "integrity": "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.0", - "@typescript-eslint/tsconfig-utils": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/project-service": "8.44.1", + "@typescript-eslint/tsconfig-utils": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3240,13 +3241,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3283,16 +3284,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", - "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.1.tgz", + "integrity": "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0" + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3303,18 +3304,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", - "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.1.tgz", + "integrity": "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.44.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12755,12 +12756,11 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index c18ed30084..89c6d77ca4 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "bench": "vitest bench", "bench:report": "vitest bench --reporter=verbose", "test": "vitest", + "test:types": "npx tsc --noEmit test/types/*.ts", "lint": "eslint .", "lint:fix": "eslint --fix .", - "generate-types": "npm run docs && node utils/generate-types.mjs && node utils/patch.mjs" + "generate-types": "npm run docs && node utils/typescript.mjs" }, "lint-staged": { "src/**/*.js": "eslint", @@ -61,6 +62,7 @@ "rollup": "^4.9.6", "rollup-plugin-string": "^3.0.0", "rollup-plugin-visualizer": "^5.12.0", + "typescript": "^5.9.2", "vite": "^5.0.2", "vite-plugin-string": "^1.2.2", "vitest": "^2.1.5", @@ -69,8 +71,17 @@ "license": "LGPL-2.1", "browser": "./lib/p5.min.js", "exports": { - ".": "./dist/app.js", - "./core": "./dist/core/main.js", + ".": { + "types": "./types/p5.d.ts", + "default": "./dist/app.js" + }, + "./global": { + "types": "./types/global.d.ts", + "default": "./dist/app.js" + }, + "./core": { + "default": "./dist/core/main.js" + }, "./shape": "./dist/shape/index.js", "./accessibility": "./dist/accessibility/index.js", "./friendlyErrors": "./dist/core/friendlyErrors/index.js", diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 888c9606d6..69dab96fb0 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -758,12 +758,16 @@ function color(p5, fn, lifecycles){ * instance of this class. * * @class p5.Color - * @param {p5} [pInst] pointer to p5 instance. + * @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){ diff --git a/src/color/setting.js b/src/color/setting.js index 23500830db..e60af75c3f 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -31,6 +31,7 @@ function setting(p5, fn){ * * @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);
- *
- * // 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.');
- * }
- *
- *
- * 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.');
- * }
- *
- *
- * function setup() {
- * background(200);
- *
- * // Display the canvas' width.
- * text(width, 42, 54);
- *
- * describe('The number 100 written in black on a gray square.');
- * }
- *
- *
- * 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.');
- * }
- *
- *
- * 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);
- * }
- * }
- *
- *
- * function setup() {
- * background(200);
- *
- * // Display the canvas' height.
- * text(height, 42, 54);
- *
- * describe('The number 100 written in black on a gray square.');
- * }
- *
- *
- * 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.');
- * }
- *
- *
- * 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);
- * }
- * }
- *
- *
+ * function setup() {
+ * background(200);
+ *
+ * // Display the canvas' width.
+ * text(width, 42, 54);
+ *
+ * describe('The number 100 written in black on a gray square.');
+ * }
+ *
+ *
+ * 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.');
+ * }
+ *
+ *
+ * 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);
+ * }
+ * }
+ *
+ *
+ * function setup() {
+ * background(200);
+ *
+ * // Display the canvas' height.
+ * text(height, 42, 54);
+ *
+ * describe('The number 100 written in black on a gray square.');
+ * }
+ *
+ *
+ * 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.');
+ * }
+ *
+ *
+ * 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);
+ * }
+ * }
+ *
+ *
@@ -200,7 +200,7 @@ function file(p5, fn){
* For example, `'image'` and `'text'` are both MIME types.
*
* @for p5.File
- * @property type
+ * @property {String} type
* @example
*
*
@@ -237,7 +237,7 @@ function file(p5, fn){
* MIME type
* may have a subtype such as ``png`` or ``jpeg``.
*
- * @property subtype
+ * @property {String} subtype
* @for p5.File
*
* @example
@@ -273,7 +273,7 @@ function file(p5, fn){
/**
* The file name as a string.
*
- * @property name
+ * @property {String} name
* @for p5.File
*
* @example
@@ -309,7 +309,7 @@ function file(p5, fn){
/**
* The number of bytes in the file.
*
- * @property size
+ * @property {Number} size
* @for p5.File
*
* @example
@@ -347,7 +347,7 @@ function file(p5, fn){
* Data can be either image data, text contents, or a parsed object in the
* case of JSON and p5.XML objects.
*
- * @property data
+ * @property {any} data
* @for p5.File
*
* @example
diff --git a/src/dom/p5.MediaElement.js b/src/dom/p5.MediaElement.js
index a8afa2a6c7..49f09dfa50 100644
--- a/src/dom/p5.MediaElement.js
+++ b/src/dom/p5.MediaElement.js
@@ -5,6 +5,20 @@
import { Element } from './p5.Element';
+/**
+ * @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;
@@ -614,6 +628,7 @@ class MediaElement extends Element {
*
* 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
@@ -690,17 +705,11 @@ class MediaElement extends Element {
*
*
*/
- /**
- * @param {Number} time time to jump to (in seconds).
- * @chainable
- */
time(val) {
- if (typeof val === 'undefined') {
- return this.elt.currentTime;
- } else {
+ if (typeof val !== 'undefined') {
this.elt.currentTime = val;
- return this;
}
+ return this.elt.currentTime;
}
/**
@@ -1385,7 +1394,8 @@ function media(p5, fn){
* The second parameter, `callback`, is optional. It's a function to call once
* the video is ready to play.
*
- * @param {String|String[]} src path to a video file, or an array of paths for
+ * @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.
@@ -1457,7 +1467,7 @@ function media(p5, fn){
*/
fn.createVideo = function (src, callback) {
// p5._validateParameters('createVideo', arguments);
- return createMedia(this, 'video', src, callback);
+ return createMedia(this, VIDEO, src, callback);
};
/* AUDIO STUFF */
@@ -1468,11 +1478,11 @@ function media(p5, fn){
* `createAudio()` returns a new
* p5.MediaElement object.
*
- * The first parameter, `src`, is the path the video. If a single string is
- * passed, as in `'assets/video.mp4'`, a single video is loaded. An array
- * of strings can be used to load the same video in different formats. For
- * example, `['assets/video.mp4', 'assets/video.ogv', 'assets/video.webm']`.
- * This is useful for ensuring that the video can play across different
+ * 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.
@@ -1480,6 +1490,7 @@ function media(p5, fn){
* 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.
@@ -1504,14 +1515,13 @@ function media(p5, fn){
*/
fn.createAudio = function (src, callback) {
// p5._validateParameters('createAudio', arguments);
- return createMedia(this, 'audio', src, callback);
+ return createMedia(this, AUDIO, src, callback);
};
/* CAMERA STUFF */
- fn.VIDEO = 'video';
-
- fn.AUDIO = 'audio';
+ 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
@@ -1574,6 +1584,7 @@ function media(p5, fn){
* 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.
@@ -1693,7 +1704,7 @@ function media(p5, fn){
const videoConstraints = { video: useVideo, audio: useAudio };
constraints = Object.assign({}, videoConstraints, constraints);
- const domElement = document.createElement('video');
+ const domElement = document.createElement(VIDEO);
// required to work in iOS 11 & up:
domElement.setAttribute('playsinline', '');
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js
index 3666ac1056..c06b5fc522 100644
--- a/src/image/loading_displaying.js
+++ b/src/image/loading_displaying.js
@@ -183,12 +183,11 @@ function loadingDisplaying(p5, fn){
* @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:
- * `delay`, a Number specifying how much time to wait before recording;
- * `units`, a String that can be either 'seconds' or 'frames'. By default it's 'seconds’;
- * `silent`, a Boolean that defines presence of progress notifications. By default it’s `false`;
- * `notificationDuration`, a Number that defines how long in seconds the final notification
- * will live. By default it's `0`, meaning the notification will never be removed;
- * `notificationID`, a String that specifies the id of the notification's DOM element. By default it’s `'progressBar’`.
+ * @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
*
diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js
index 897e1b02d8..20d9dd23b1 100644
--- a/src/image/p5.Image.js
+++ b/src/image/p5.Image.js
@@ -209,12 +209,12 @@ class Image {
* 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.
+ * @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
*
diff --git a/src/image/pixels.js b/src/image/pixels.js
index ebea101273..f88818faed 100644
--- a/src/image/pixels.js
+++ b/src/image/pixels.js
@@ -8,142 +8,6 @@
import Filters from './filters';
function pixels(p5, fn){
- /**
- * 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.');
- * }
- *
- *
- *
- *
- *
- * 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.');
- * }
- *
- *
- *
- *
- *
- * 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.');
- * }
- *
- *
- */
-
/**
* Copies a region of pixels from one image to another.
*
@@ -1116,6 +980,142 @@ function pixels(p5, fn){
}
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.');
+ * }
+ *
+ *
+ *
+ *
+ *
+ * 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.');
+ * }
+ *
+ *
+ *
+ *
+ *
+ * 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.');
+ * }
+ *
+ *
+ */
}
export default pixels;
diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js
index 2b033456e0..d8a5b679a1 100644
--- a/src/math/p5.Vector.js
+++ b/src/math/p5.Vector.js
@@ -864,7 +864,6 @@ class Vector {
* p5.Vector object and doesn't change the
* originals.
*
- * @method mult
* @param {Number} n The number to multiply with the vector
* @chainable
* @example
@@ -1591,7 +1590,6 @@ class Vector {
* Use dist() to calculate the distance between points
* using coordinates as in `dist(x1, y1, x2, y2)`.
*
- * @method dist
* @submodule p5.Vector
* @param {p5.Vector} v x, y, and z coordinates of a p5.Vector.
* @return {Number} distance.
@@ -3060,7 +3058,6 @@ class Vector {
*
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON
*
- * @method clampToZero
* @return {p5.Vector} with components very close to zero replaced with zero.
* @chainable
*/
diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js
index 47e7e2c773..608ac60743 100644
--- a/src/math/trigonometry.js
+++ b/src/math/trigonometry.js
@@ -9,7 +9,91 @@
import * as constants from '../core/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';
/*
diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js
index 3eb76c8ff6..eccfc74170 100644
--- a/src/strands/strands_builtins.js
+++ b/src/strands/strands_builtins.js
@@ -1,4 +1,6 @@
-import { GenType, DataType } from "./ir_types"
+// Need the .js extension because we also import this from a Node script.
+// Try to keep this file minimal because of that.
+import { GenType, DataType } from "./ir_types.js"
// GLSL Built in functions
// https://docs.gl/el3/abs
@@ -83,7 +85,7 @@ const builtInGLSLFunctions = {
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}],
@@ -103,7 +105,7 @@ const builtInGLSLFunctions = {
],
reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}],
-
+
////////// Texture sampling //////////
texture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}],
getTexture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}]
@@ -111,4 +113,4 @@ const builtInGLSLFunctions = {
export const strandsBuiltinFunctions = {
...builtInGLSLFunctions,
-}
\ No newline at end of file
+}
diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js
index 96e574aa13..1ffa018185 100644
--- a/src/type/p5.Font.js
+++ b/src/type/p5.Font.js
@@ -109,7 +109,6 @@ export class Font {
* @param {Number} y y‐coordinate of the text baseline.
* @param {Number} [width] Optional width for text wrapping.
* @param {Number} [height] Optional height for text wrapping.
- * @param {Object} [options] Configuration object for rendering text.
* @return {Array} A flat array of path commands.
*
* @example
@@ -241,8 +240,9 @@ export class Font {
* @param {String} str string of text.
* @param {Number} x x-coordinate of the text.
* @param {Number} y y-coordinate of the text.
- * @param {Object} [options] object with sampleFactor and simplifyThreshold
- * properties.
+ * @param {Object} [options] Configuration:
+ * @param {Number} [options.sampleFactor=0.1] The ratio of the text's path length to the number of samples.
+ * @param {Number} [options.simplifyThreshold=0] A minmum angle between two segments. Segments with a shallower angle will be merged.
* @return {Array
*/
- /*
- * @method textLeading
- * @for p5
- */
/**
* Sets the font used by the text() function.
@@ -761,7 +757,7 @@ function textCore(p5, fn) {
*
* 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")`.
*
diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js
index 8fa21dae9c..b8172e876b 100644
--- a/src/webgl/interaction.js
+++ b/src/webgl/interaction.js
@@ -69,9 +69,9 @@ function interaction(p5, fn){
* @param {Number} [sensitivityX] sensitivity to movement along the x-axis. Defaults to 1.
* @param {Number} [sensitivityY] sensitivity to movement along the y-axis. Defaults to 1.
* @param {Number} [sensitivityZ] sensitivity to movement along the z-axis. Defaults to 1.
- * @param {Object} [options] object with two optional properties, `disableTouchActions`
- * and `freeRotation`. Both are `Boolean`s. `disableTouchActions`
- * defaults to `true` and `freeRotation` defaults to `false`.
+ * @param {Object} [options] Settings for orbitControl:
+ * @param {Boolean} [options.disableTouchActions=true] Prevent accidental interactions with the page while orbiting.
+ * @param {Boolean} [options.freeRotation=false] Rotate in the drag direction instead of on principal axes.
* @chainable
*
* @example
diff --git a/src/webgl/light.js b/src/webgl/light.js
index 3aaf09e3e2..21f9b74972 100644
--- a/src/webgl/light.js
+++ b/src/webgl/light.js
@@ -895,7 +895,7 @@ function light(p5, fn){
* use as the light source.
*
* @method imageLight
- * @param {p5.image} img image to use as the light source.
+ * @param {p5.Image} img image to use as the light source.
*
* @example
*
diff --git a/src/webgl/loading.js b/src/webgl/loading.js
index e808cd4275..098211f822 100755
--- a/src/webgl/loading.js
+++ b/src/webgl/loading.js
@@ -99,7 +99,7 @@ function loading(p5, fn){
* @method loadModel
* @param {String|Request} path path of the model to be loaded.
* @param {String} [fileType] model’s file extension. Either `'.obj'` or `'.stl'`.
- * @param {Boolean} normalize if `true`, scale the model to fit the canvas.
+ * @param {Boolean} [normalize] if `true`, scale the model to fit the canvas.
* @param {function(p5.Geometry)} [successCallback] function to call once the model is loaded. Will be passed
* the p5.Geometry object.
* @param {function(Event)} [failureCallback] function to call if the model fails to load. Will be passed an `Error` event object.
@@ -1137,7 +1137,7 @@ function loading(p5, fn){
* @param {String} modelString String of the object to be loaded
* @param {String} [fileType] The file extension of the model
* (.stl
, .obj
).
- * @param {Boolean} normalize If true, scale the model to a
+ * @param {Boolean} [normalize] If true, scale the model to a
* standardized size when loading
* @param {function(p5.Geometry)} [successCallback] Function to be called
* once the model is loaded. Will be passed
diff --git a/src/webgl/material.js b/src/webgl/material.js
index 917402faae..b511257ae4 100644
--- a/src/webgl/material.js
+++ b/src/webgl/material.js
@@ -214,8 +214,8 @@ function material(p5, fn){
* @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:
- * - `vertex`: An object describing the available vertex shader hooks.
- * - `fragment`: An object describing the available frament shader hooks.
+ * @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.
*
diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js
index b687f916c5..9455d6a26e 100644
--- a/src/webgl/p5.Camera.js
+++ b/src/webgl/p5.Camera.js
@@ -19,357 +19,712 @@ class Camera {
this.projMatrix = new Matrix(4);
this.yScale = 1;
}
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // Camera Projection Methods
+ ////////////////////////////////////////////////////////////////////////////////
+
/**
- * The camera’s x-coordinate.
+ * Sets a perspective projection for the camera.
*
- * By default, the camera’s x-coordinate is set to 0 in "world" space.
+ * 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.
*
- * @property {Number} eyeX
- * @readonly
+ * `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
*
*
- * let cam;
- * let font;
+ * // Double-click to toggle between cameras.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Set the camera
- * setCamera(cam);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // Place the camera at the top-center.
- * cam.setPosition(0, -400, 800);
+ * // Place it at the top-right.
+ * cam2.camera(400, -400, 800);
*
- * // Point the camera at the origin.
- * cam.lookAt(0, 0, 0);
+ * // 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);
*
- * describe(
- * 'A white cube on a gray background. The text "eyeX: 0" is written in black beneath it.'
- * );
+ * // 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);
*
- * // 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);
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
* }
*
*
*
*
*
- * let cam;
- * let font;
+ * // Double-click to toggle between cameras.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Place the camera at the top-center.
- * cam.setPosition(0, -400, 800);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // Point the camera at the origin.
- * cam.lookAt(0, 0, 0);
+ * // Place it at the top-right.
+ * cam2.camera(400, -400, 800);
*
- * 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.'
- * );
+ * // 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);
*
- * // Style the box.
- * fill(255);
+ * // Translate the origin left and right.
+ * let x = 100 * sin(frameCount * 0.01);
+ * translate(x, 0, 0);
*
* // 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);
+ * // 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) {
+ 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);
+
+ this.projMatrix.set(f / aspect, 0, 0, 0,
+ 0, -f * this.yScale, 0, 0,
+ 0, 0, (far + near) * nf, -1,
+ 0, 0, (2 * far * near) * nf, 0);
+
+ if (this._isActive()) {
+ this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
+ this._renderer.states.uPMatrix.set(this.projMatrix);
+ }
+ }
/**
- * The camera’s y-coordinate.
+ * Sets an orthographic projection for the camera.
*
- * By default, the camera’s y-coordinate is set to 0 in "world" space.
+ * 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.
*
- * @property {Number} eyeY
- * @readonly
+ * `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.
*
- * @example
- *
- *
- * let cam;
- * let font;
+ * 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)`.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * 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 a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Place the camera at the top-center.
- * cam.setPosition(0, -400, 800);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // Point the camera at the origin.
- * cam.lookAt(0, 0, 0);
+ * // Apply an orthographic projection.
+ * cam2.ortho();
*
- * // Set the camera.
- * setCamera(cam);
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
*
- * describe(
- * 'A white cube on a gray background. The text "eyeY: -400" is written in black beneath it.'
- * );
+ * 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);
*
- * // Style the box.
- * fill(255);
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
*
- * // Draw the box.
- * box();
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
*
- * // Display the value of eyeY, rounded to the nearest integer.
- * text(`eyeY: ${round(cam.eyeY)}`, 0, 45);
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
* }
*
*
*
*
*
- * let cam;
- * let font;
+ * // Double-click to toggle between cameras.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Set the camera
- * setCamera(cam);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // Place the camera at the top-center.
- * cam.setPosition(0, -400, 800);
+ * // Apply an orthographic projection.
+ * cam2.ortho();
*
- * // Point the camera at the origin.
- * cam.lookAt(0, 0, 0);
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
*
- * 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.'
- * );
+ * 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);
*
- * // Style the box.
- * fill(255);
- *
- * // Draw the box.
- * box();
- *
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
*
- * // Calculate the new y-coordinate.
- * let y = 25 * sin(frameCount * 0.01) - 400;
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
*
- * // Set the camera's position.
- * cam.setPosition(0, y, 800);
+ * // 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();
+ * }
+ * }
*
- * // Display the value of eyeY, rounded to the nearest integer.
- * text(`eyeY: ${round(cam.eyeY)}`, 0, 45);
+ * // 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.0 / w;
+ const y = +2.0 / h * this.yScale;
+ const z = -2.0 / 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';
+ }
/**
- * The camera’s z-coordinate.
+ * Sets the camera's frustum.
*
- * By default, the camera’s z-coordinate is set to 800 in "world" space.
+ * 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.
*
- * @property {Number} eyeZ
- * @readonly
+ * `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
*
*
- * 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);
+ * // Double-click to toggle between cameras.
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
*
- * // Set the camera
- * setCamera(cam);
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
*
- * // Place the camera at the top-center.
- * cam.setPosition(0, -400, 800);
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Point the camera at the origin.
- * cam.lookAt(0, 0, 0);
+ * // 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 white cube on a gray background. The text "eyeZ: 800" is written in black beneath it.'
+ * '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);
*
- * // Style the box.
- * fill(255);
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
*
- * // Draw the box.
- * box();
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
*
- * // Display the value of eyeZ, rounded to the nearest integer.
- * text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45);
+ * // 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 font;
+ * let delta = 0.01;
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * function setup() {
* createCanvas(100, 100, WEBGL);
- *
+ * normalMaterial();
* // 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);
+ * // Roll camera according to angle 'delta'
+ * cam.roll(delta);
*
- * // Display the value of eyeZ, rounded to the nearest integer.
- * text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45);
+ * 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
+ );
+ }
/**
- * The x-coordinate of the place where the camera looks.
+ * Rotates the camera left and right.
*
- * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
- * `myCamera.centerX` is 0.
+ * Panning rotates the camera without changing its position. The rotation
+ * happens in the camera’s "local" space.
*
- * @property {Number} centerX
- * @readonly
+ * 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 font;
+ * let delta = 0.001;
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Camera object.
@@ -381,43 +736,60 @@ class Camera {
* // Place the camera at the top-center.
* cam.setPosition(0, -400, 800);
*
- * // Point the camera at (10, 20, -30).
- * cam.lookAt(10, 20, -30);
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
*
* describe(
- * 'A white cube on a gray background. The text "centerX: 10" is written in black beneath it.'
+ * '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);
*
- * // Style the box.
- * fill(255);
- *
- * // Draw the box.
- * box();
+ * // Pan with the camera.
+ * cam.pan(delta);
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Switch directions every 120 frames.
+ * if (frameCount % 120 === 0) {
+ * delta *= -1;
+ * }
*
- * // Display the value of centerX, rounded to the nearest integer.
- * text(`centerX: ${round(cam.centerX)}`, 0, 45);
+ * // 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 font;
+ * let delta = 0.001;
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Camera object.
@@ -426,63 +798,64 @@ class Camera {
* // Set the camera
* setCamera(cam);
*
- * // Place the camera at the top-right.
- * cam.setPosition(100, -400, 800);
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
*
- * // Point the camera at (10, 20, -30).
- * cam.lookAt(10, 20, -30);
+ * // 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 shifts its focus. The text "centerX: X" is written in black beneath the cube. X oscillates between -15 and 35.'
+ * '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);
*
- * // Style the box.
- * fill(255);
+ * // Pan with the camera.
+ * cam.tilt(delta);
+ *
+ * // Switch directions every 120 frames.
+ * if (frameCount % 120 === 0) {
+ * delta *= -1;
+ * }
*
* // 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);
* }
*
*
*/
+ tilt(amount) {
+ const local = this._getLocalAxes();
+ this._rotateView(amount, local.x[0], local.x[1], local.x[2]);
+ }
/**
- * The y-coordinate of the place where the camera looks.
+ * Points the camera at a location.
*
- * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
- * `myCamera.centerY` is 0.
+ * `myCamera.lookAt()` changes the camera’s orientation without changing its
+ * position.
*
- * @property {Number} centerY
- * @readonly
+ * 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 font;
+ * let isLookingLeft = true;
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Camera object.
@@ -494,1267 +867,1326 @@ class Camera {
* // Place the camera at the top-center.
* cam.setPosition(0, -400, 800);
*
- * // Point the camera at (10, 20, -30).
- * cam.lookAt(10, 20, -30);
+ * // Point the camera at the origin.
+ * cam.lookAt(-30, 0, 0);
*
* describe(
- * 'A white cube on a gray background. The text "centerY: 20" is written in black beneath it.'
+ * '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);
- *
+ * fill(255, 0, 0);
* // Draw the box.
- * box();
+ * box(20);
+ * pop();
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // 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();
+ * }
*
- * // Display the value of centerY, rounded to the nearest integer.
- * text(`centerY: ${round(cam.centerY)}`, 0, 45);
+ * // 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
*
*
- * let cam;
- * let font;
+ * // Double-click to toggle between cameras.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Set the camera
- * setCamera(cam);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // Place the camera at the top-right.
- * cam.setPosition(100, -400, 800);
+ * // 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);
*
- * // Point the camera at (10, 20, -30).
- * cam.lookAt(10, 20, -30);
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
*
* 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.'
+ * '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);
*
- * // Style the box.
- * fill(255);
- *
- * // Draw the box.
- * box();
- *
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
*
- * // Calculate the new y-coordinate.
- * let y = 25 * sin(frameCount * 0.01) + 20;
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
*
- * // Point the camera.
- * cam.lookAt(10, y, -30);
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -30);
+ * box(10);
+ * }
+ * }
*
- * // Display the value of centerY, rounded to the nearest integer.
- * text(`centerY: ${round(cam.centerY)}`, 0, 45);
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
* }
*
*
- */
-
- /**
- * 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
- * @readonly
*
- * @example
*
*
- * let cam;
- * let font;
+ * // Double-click to toggle between cameras.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Set the camera
- * setCamera(cam);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // Place the camera at the top-center.
- * cam.setPosition(0, -400, 800);
+ * // 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);
*
- * // Point the camera at (10, 20, -30).
- * cam.lookAt(10, 20, -30);
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
*
* describe(
- * 'A white cube on a gray background. The text "centerZ: -30" is written in black beneath it.'
+ * '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);
*
- * // Style the box.
- * fill(255);
+ * // 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);
*
- * // Draw the box.
- * box();
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
*
- * // Display the value of centerZ, rounded to the nearest integer.
- * text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
+ * // 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;
- * let font;
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
+ * // Create the first camera.
+ * // Keep its default settings.
* cam = createCamera();
*
* // Place the camera at the top-right.
- * cam.setPosition(100, -400, 800);
+ * cam.setPosition(400, -400, 800);
*
- * // Point the camera at (10, 20, -30).
- * cam.lookAt(10, 20, -30);
+ * // Point it at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * // Set the camera.
+ * setCamera(cam);
*
* 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.'
+ * 'A white cube drawn against a gray background. The cube appears to move when the user presses certain keys.'
* );
* }
*
* function draw() {
* background(200);
*
- * // Style the box.
- * fill(255);
- *
- * // Draw the box.
- * box();
+ * // Move the camera along its "local" axes
+ * // when the user presses certain keys.
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Move horizontally.
+ * if (keyIsDown(LEFT_ARROW)) {
+ * cam.move(-1, 0, 0);
+ * }
+ * if (keyIsDown(RIGHT_ARROW)) {
+ * cam.move(1, 0, 0);
+ * }
*
- * // Calculate the new z-coordinate.
- * let z = 25 * sin(frameCount * 0.01) - 30;
+ * // Move vertically.
+ * if (keyIsDown(UP_ARROW)) {
+ * cam.move(0, -1, 0);
+ * }
+ * if (keyIsDown(DOWN_ARROW)) {
+ * cam.move(0, 1, 0);
+ * }
*
- * // Point the camera.
- * cam.lookAt(10, 20, z);
+ * // Move in/out of the screen.
+ * if (keyIsDown('i')) {
+ * cam.move(0, 0, -1);
+ * }
+ * if (keyIsDown('o')) {
+ * cam.move(0, 0, 1);
+ * }
*
- * // Display the value of centerZ, rounded to the nearest integer.
- * text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
+ * // Draw the box.
+ * box();
* }
*
*
*/
+ move(x, y, z) {
+ const local = this._getLocalAxes();
- /**
- * 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
- * @readonly
- *
- * @example
- *
- *
- * let cam;
- * let font;
- *
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ // 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 a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Set the camera
- * setCamera(cam);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // 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);
+ * // Place it closer to the origin.
+ * cam2.setPosition(0, 0, 600);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
*
* describe(
- * 'A white cube on a gray background. The text "upX: 0" is written in black beneath it.'
+ * '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);
*
- * // Style the box.
- * fill(255);
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
*
- * // Draw the box.
- * box();
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -30);
+ * box(10);
+ * }
+ * }
*
- * // Display the value of upX, rounded to the nearest tenth.
- * text(`upX: ${round(cam.upX, 1)}`, 0, 45);
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
* }
*
*
*
*
*
- * let cam;
- * let font;
+ * // Double-click to toggle between cameras.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
*
- * // Set the camera
- * setCamera(cam);
+ * // Create the second camera.
+ * cam2 = createCamera();
*
- * // 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);
+ * // Place it closer to the origin.
+ * cam2.setPosition(0, 0, 600);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
*
* 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.'
+ * '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);
*
- * // Style the box.
- * fill(255);
- *
- * // Draw the box.
- * box();
+ * // Update cam2's z-coordinate.
+ * let z = 100 * sin(frameCount * 0.01) + 700;
+ * cam2.setPosition(0, 0, z);
*
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
*
- * // Calculate the x-component.
- * let x = sin(frameCount * 0.01);
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
*
- * // Update the camera's "up" vector.
- * cam.camera(100, -400, 800, 0, 0, 0, x, 1, 0);
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -30);
+ * box(10);
+ * }
+ * }
*
- * // Display the value of upX, rounded to the nearest tenth.
- * text(`upX: ${round(cam.upX, 1)}`, 0, 45);
+ * // 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
+ );
+ }
/**
- * The y-component of the camera's "up" vector.
+ * Sets the camera’s position, orientation, and projection by copying another
+ * camera.
*
- * 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.
+ * The parameter, `cam`, is the `p5.Camera` object to copy. For example, calling
+ * `cam2.set(cam1)` will set `cam2` using `cam1`’s configuration.
*
- * @property {Number} upY
- * @readonly
+ * @param {p5.Camera} cam camera to copy.
*
* @example
*
*
- * let cam;
- * let font;
+ * // Double-click to "reset" the camera zoom.
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * let cam1;
+ * let cam2;
+ *
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Create the first camera.
+ * cam1 = createCamera();
*
- * // Set the camera
- * setCamera(cam);
+ * // Place the camera at the top-right.
+ * cam1.setPosition(400, -400, 800);
*
- * // 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);
+ * // 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 on a gray background. The text "upY: 1" is written in black beneath it.'
+ * '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);
*
- * // Style the box.
- * fill(255);
+ * // Update cam2's position.
+ * cam2.move(0, 0, -1);
*
* // 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);
* }
- *
- *
- *
- *
- *
- * 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.'
- * );
+ * // "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.
*
- * function draw() {
- * background(200);
- *
- * // Style the box.
- * fill(255);
- *
- * // Draw the box.
- * box();
- *
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * textFont(font);
- * fill(0);
+ * `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.
*
- * // Calculate the y-component.
- * let y = sin(frameCount * 0.01);
+ * The first two parameters, `cam0` and `cam1`, are the `p5.Camera` objects
+ * that should be used to set the current camera.
*
- * // Update the camera's "up" vector.
- * cam.camera(100, -400, 800, 0, 0, 0, 0, y, 0);
+ * 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.
*
- * // 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.
+ * 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.
*
- * 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.
+ * Note: All of the cameras must use the same projection.
*
- * @property {Number} upZ
- * @readonly
+ * @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 font;
+ * let cam0;
+ * let cam1;
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
+ * function setup() {
* createCanvas(100, 100, WEBGL);
*
- * // Create a p5.Camera object.
+ * // Create the main camera.
+ * // Keep its default settings.
* 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);
- * }
- *
- *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam0 = createCamera();
*
- *
- *
- * let cam;
- * let font;
+ * // Create the second camera.
+ * cam1 = createCamera();
*
- * async function setup() {
- * // Load a font and create a p5.Font object.
- * font = await loadFont('assets/inconsolata.otf');
- * createCanvas(100, 100, WEBGL);
+ * // Place it at the top-right.
+ * cam1.setPosition(400, -400, 800);
*
- * // Create a p5.Camera object.
- * cam = createCamera();
+ * // Point it at the origin.
+ * cam1.lookAt(0, 0, 0);
*
- * // Set the camera
+ * // Set the current camera to cam.
* 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.'
- * );
+ * 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);
*
- * // 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);
+ * // Calculate the amount to interpolate between cam0 and cam1.
+ * let amt = 0.5 * sin(frameCount * 0.01) + 0.5;
*
- * // Update the camera's "up" vector.
- * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, z);
+ * // Update the main camera's position and orientation.
+ * cam.slerp(cam0, cam1, amt);
*
- * // Display the value of upZ, rounded to the nearest tenth.
- * text(`upZ: ${round(cam.upZ, 1)}`, 0, 45);
+ * 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;
+ }
- ////////////////////////////////////////////////////////////////////////////////
- // Camera Projection Methods
- ////////////////////////////////////////////////////////////////////////////////
+ // 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._renderer.states.uPMatrix.clone());
+ this._renderer.states.uPMatrix.mat4 = this.projMatrix.mat4.slice();
+ }
+ }
- /**
- * 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;
- * }
- * }
- *
- *
- *
- *
- *
- * // 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) {
- 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;
- }
+ // 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);
- if (near <= 0.0001) {
- near = 0.01;
- console.log(
- 'Avoid perspective near plane values close to or below 0. ' +
- 'Setting value to 0.01.'
- );
- }
+ // 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);
- if (far < near) {
- console.log(
- 'Perspective far plane value is less than near plane value. ' +
- 'Nothing will be shown.'
- );
+ // 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));
}
- this.aspectRatio = aspect;
- this.cameraNear = near;
- this.cameraFar = far;
+ // 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
+ );
- this.projMatrix = new Matrix(4);
+ // Prepare each of rotation matrix from their camera matrix
+ const rotMat0 = cam0.cameraMatrix.createSubMatrix3x3();
+ const rotMat1 = cam1.cameraMatrix.createSubMatrix3x3();
- const f = 1.0 / Math.tan(this.cameraFOV / 2);
- const nf = 1.0 / (this.cameraNear - this.cameraFar);
+ // 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);
- this.projMatrix.set(f / aspect, 0, 0, 0,
- 0, -f * this.yScale, 0, 0,
- 0, 0, (far + near) * nf, -1,
- 0, 0, (2 * far * near) * nf, 0);
+ // prepare new vectors.
+ const newFront = new Vector();
+ const newUp = new Vector();
+ const newEye = new Vector();
+ const newCenter = new Vector();
- if (this._isActive()) {
- this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
- this._renderer.states.uPMatrix.set(this.projMatrix);
+ // 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;
}
- }
- /**
- * 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;
- * }
- * }
- *
- *
- *
- *
- *
- * // 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.0 / w;
- const y = +2.0 / h * this.yScale;
- const z = -2.0 / d;
- const tx = -(right + left) / w;
- const ty = -(top + bottom) / h;
- const tz = -(far + near) / d;
- this.projMatrix = new Matrix(4);
+ // 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];
- this.projMatrix.set(x, 0, 0, 0,
- 0, -y, 0, 0,
- 0, 0, z, 0,
- tx, ty, tz, 1);
+ 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;
- if (this._isActive()) {
- this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
- this._renderer.states.uPMatrix.set(this.projMatrix);
+ } 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;
}
- this.cameraType = 'custom';
+
+ // 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
+ );
}
- /**
- * 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;
+ ////////////////////////////////////////////////////////////////////////////////
+ // Camera Helper Methods
+ ////////////////////////////////////////////////////////////////////////////////
- const w = right - left;
- const h = top - bottom;
- const d = far - near;
+ // @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 * 0.1;
+ this.defaultCameraFar = this.defaultEyeZ * 10;
+ }
- const x = +(2.0 * near) / w;
- const y = +(2.0 * near) / h * this.yScale;
- const z = -(2.0 * far * near) / d;
+ //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;
- const tx = (right + left) / w;
- const ty = (top + bottom) / h;
- const tz = -(far + near) / d;
+ this.perspective();
+ this.camera();
- this.projMatrix = new Matrix(4);
+ 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();
+ }
+ }
- this.projMatrix.set(x, 0, 0, 0,
- 0, -y, 0, 0,
- tx, ty, tz, -1,
- 0, 0, z, 0);
+ /**
+ * 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;
- if (this._isActive()) {
- this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
- this._renderer.states.uPMatrix.set(this.projMatrix);
- }
+ _cam.cameraMatrix = this.cameraMatrix.copy();
+ _cam.projMatrix = this.projMatrix.copy();
+ _cam.yScale = this.yScale;
- this.cameraType = 'custom';
+ return _cam;
}
- ////////////////////////////////////////////////////////////////////////////////
- // Camera Orientation Methods
- ////////////////////////////////////////////////////////////////////////////////
+ clone() {
+ return this.copy();
+ }
/**
- * Rotate camera view about arbitrary axis defined by x,y,z
- * based on http://learnwebgl.brown37.net/07_cameras/camera_rotating_motion.html
+ * Returns a camera's local axes: left-right, up-down, and forward-backward,
+ * as defined by vectors in world-space.
* @private
*/
- _rotateView(a, x, y, z) {
- let centerX = this.centerX;
- let centerY = this.centerY;
- let centerZ = this.centerZ;
+ _getLocalAxes() {
+ // calculate camera local Z vector
+ let z0 = this.eyeX - this.centerX;
+ let z1 = this.eyeY - this.centerY;
+ let z2 = this.eyeZ - this.centerZ;
- // move center by eye position such that rotation happens around eye position
- centerX -= this.eyeX;
- centerY -= this.eyeY;
- centerZ -= this.eyeZ;
+ // normalize camera local Z vector
+ const eyeDist = Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
+ if (eyeDist !== 0) {
+ z0 /= eyeDist;
+ z1 /= eyeDist;
+ z2 /= eyeDist;
+ }
- const rotation = new Matrix(4); // TODO Maybe pass p5
- rotation.rotate4x4(this._renderer._pInst._toRadians(a), x, y, z);
+ // calculate camera Y vector
+ let y0 = this.upX;
+ let y1 = this.upY;
+ let y2 = this.upZ;
- 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]
- ];
+ // 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;
- // add eye position back into center
- rotatedCenter[0] += this.eyeX;
- rotatedCenter[1] += this.eyeY;
- rotatedCenter[2] += this.eyeZ;
+ // recompute y = z cross x
+ y0 = z1 * x2 - z2 * x1;
+ y1 = -z0 * x2 + z2 * x0;
+ y2 = z0 * x1 - z1 * x0;
- this.camera(
- this.eyeX,
- this.eyeY,
- this.eyeZ,
- rotatedCenter[0],
- rotatedCenter[1],
- rotatedCenter[2],
- this.upX,
- this.upY,
- this.upZ
+ // 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
);
}
/**
- * Rotates the camera in a clockwise/counter-clockwise direction.
+ * 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.
*
- * Rolling rotates the camera without changing its orientation. The rotation
- * happens in the camera’s "local" space.
+ * `camera()` allows objects to be viewed from different angles. It has nine
+ * parameters that are all optional.
*
- * 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.
+ * 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)`.
*
- * Note: Angles are interpreted based on the current
- * angleMode().
+ * 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
*
- * @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);
+ * describe('A white cube on a gray background.');
* }
*
* function draw() {
* background(200);
*
- * // Roll camera according to angle 'delta'
- * cam.roll(delta);
+ * // Move the camera to the top-right.
+ * camera(200, -400, 800);
*
- * 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);
- * }
+ * // Draw the box.
+ * box();
+ * }
*
*
*
- * @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.
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
*
- * Panning rotates the camera without changing its position. The rotation
- * happens in the camera’s "local" space.
+ * describe('A white cube apperas to sway left and right on a gray background.');
+ * }
*
- * 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.
+ * function draw() {
+ * background(200);
*
- * Note: Angles are interpreted based on the current
- * angleMode().
+ * // Calculate the camera's x-coordinate.
+ * let x = 400 * cos(frameCount * 0.01);
*
- * @param {Number} angle amount to rotate in the current
- * angleMode().
+ * // Orbit the camera around the box.
+ * camera(x, -400, 800);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ *
*
- * @example
*
*
- * let cam;
- * let delta = 0.001;
+ * // Adjust the range sliders to change the camera's position.
+ *
+ * let xSlider;
+ * let ySlider;
+ * let zSlider;
*
* 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);
+ * // 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 on a gray background. The cube goes in and out of view as the camera pans left and right.'
+ * '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);
*
- * // Pan with the camera.
- * cam.pan(delta);
+ * // Get the camera's coordinates from the sliders.
+ * let x = xSlider.value();
+ * let y = ySlider.value();
+ * let z = zSlider.value();
*
- * // Switch directions every 120 frames.
- * if (frameCount % 120 === 0) {
- * delta *= -1;
- * }
+ * // Move the camera.
+ * camera(x, y, z);
*
* // Draw the box.
* box();
@@ -1762,240 +2194,189 @@ class Camera {
*
*
*/
- pan(amount) {
- const local = this._getLocalAxes();
- this._rotateView(amount, local.y[0], local.y[1], local.y[2]);
- }
+ fn.camera = function (...args) {
+ this._assert3d('camera');
+ // p5._validateParameters('camera', args);
+ this._renderer.camera(...args);
+ return this;
+ };
/**
- * Rotates the camera up and down.
+ * Sets a perspective projection for the current camera in a 3D sketch.
*
- * Tilting rotates the camera without changing its position. The rotation
- * happens in the camera’s "local" space.
+ * 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.
*
- * 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.
+ * `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.
*
- * Note: Angles are interpreted based on the current
- * angleMode().
+ * 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)`.
*
- * @param {Number} angle amount to rotate in the current
- * angleMode().
+ * 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
*
*
- * let cam;
- * let delta = 0.001;
+ * // Double-click to squeeze the box.
+ *
+ * let isSqueezed = false;
*
* 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.'
- * );
+ * describe('A white rectangular prism on a gray background. The box appears to become thinner when the user double-clicks.');
* }
*
* function draw() {
* background(200);
*
- * // Pan with the camera.
- * cam.tilt(delta);
+ * // Place the camera at the top-right.
+ * camera(400, -400, 800);
*
- * // Switch directions every 120 frames.
- * if (frameCount % 120 === 0) {
- * delta *= -1;
+ * 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;
+ * }
*
*
- */
- 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.'
- * );
+ * describe('A white rectangular prism on a gray background. The prism moves away from the camera until it disappears.');
* }
*
* 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();
+ * // Place the camera at the top-right.
+ * camera(400, -400, 800);
*
- * // 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();
- * }
+ * // 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);
*
- * // 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;
- * }
+ * // 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();
* }
*
*
*/
- lookAt(x, y, z) {
- this.camera(
- this.eyeX,
- this.eyeY,
- this.eyeZ,
- x,
- y,
- z,
- this.upX,
- this.upY,
- this.upZ
- );
- }
+ fn.perspective = function (...args) {
+ this._assert3d('perspective');
+ // p5._validateParameters('perspective', args);
+ this._renderer.perspective(...args);
+ return this;
+ };
- ////////////////////////////////////////////////////////////////////////////////
- // Camera Position Methods
- ////////////////////////////////////////////////////////////////////////////////
/**
- * Sets the position and orientation of the camera.
+ * Enables or disables perspective for lines in 3D sketches.
*
- * `myCamera.camera()` allows objects to be viewed from different angles. It
- * has nine parameters that are all optional.
+ * 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.
*
- * 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)`.
+ * 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.
*
- * 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)`.
+ * `linePerspective()` can override the default line drawing mode.
*
- * 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)`.
+ * 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.
*
- * @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.
+ * 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 to toggle between cameras.
- *
- * let cam1;
- * let cam2;
- * let isDefaultCamera = true;
+ * // Double-click the canvas to toggle the line perspective.
*
* 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.'
+ * 'A white cube with black edges on a gray background. Its edges toggle between thick and thin when the user double-clicks.'
* );
* }
*
@@ -2003,7 +2384,7 @@ class Camera {
* background(200);
*
* // Translate the origin toward the camera.
- * translate(-10, 10, 500);
+ * translate(-10, 10, 600);
*
* // Rotate the coordinate system.
* rotateY(-0.1);
@@ -2011,65 +2392,39 @@ class Camera {
*
* // Draw the row of boxes.
* for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -30);
+ * translate(0, 0, -40);
* box(10);
* }
* }
*
- * // Toggle the current camera when the user double-clicks.
+ * // Toggle the line perspective when the user double-clicks.
* function doubleClicked() {
- * if (isDefaultCamera === true) {
- * setCamera(cam2);
- * isDefaultCamera = false;
- * } else {
- * setCamera(cam1);
- * isDefaultCamera = true;
- * }
+ * let isEnabled = linePerspective();
+ * linePerspective(!isEnabled);
* }
*
*
*
*
*
- * // Double-click to toggle between cameras.
- *
- * let cam1;
- * let cam2;
- * let isDefaultCamera = true;
+ * // Double-click the canvas to toggle the line perspective.
*
* 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.'
+ * '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);
*
- * // 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);
+ * // Use an orthographic projection.
+ * ortho();
*
* // Translate the origin toward the camera.
- * translate(-10, 10, 500);
+ * translate(-10, 10, 600);
*
* // Rotate the coordinate system.
* rotateY(-0.1);
@@ -2077,1786 +2432,1481 @@ class Camera {
*
* // Draw the row of boxes.
* for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -30);
+ * translate(0, 0, -40);
* box(10);
* }
* }
*
- * // Toggle the current camera when the user double-clicks.
+ * // Toggle the line perspective 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;
- * }
- * }
- *
- *
- *
- *
- *
- * // 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._renderer.states.uPMatrix.clone());
- this._renderer.states.uPMatrix.mat4 = this.projMatrix.mat4.slice();
- }
- }
-
- // 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 * 0.1;
- this.defaultCameraFar = this.defaultEyeZ * 10;
- }
-
- //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();
- }
- }
-
+ * let isEnabled = linePerspective();
+ * linePerspective(!isEnabled);
+ * }
+ *
+ *
+ */
/**
- * Returns a copy of a camera.
- * @private
+ * @method linePerspective
+ * @return {boolean} whether line perspective is enabled.
*/
- 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;
+ fn.linePerspective = function (enable) {
+ // p5._validateParameters('linePerspective', arguments);
+ if (!(this._renderer instanceof RendererGL)) {
+ throw new Error('linePerspective() must be called in WebGL mode.');
+ }
+ return this._renderer.linePerspective(enable);
+ };
- _cam.cameraMatrix = this.cameraMatrix.copy();
- _cam.projMatrix = this.projMatrix.copy();
- _cam.yScale = this.yScale;
- return _cam;
- }
+ /**
+ * 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);
+ * }
+ * }
+ *
+ *
+ *
+ *
+ *
+ * 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;
+ };
- clone() {
- return this.copy();
- }
+ /**
+ * 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);
+ * }
+ * }
+ *
+ *
+ *
+ *
+ *
+ * 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;
+ };
/**
- * Returns a camera's local axes: left-right, up-down, and forward-backward,
- * as defined by vectors in world-space.
- * @private
+ * 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;
+ * }
+ * }
+ *
+ *
*/
- _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;
- }
+ fn.createCamera = function () {
+ this._assert3d('createCamera');
- const ymag = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
- if (ymag !== 0) {
- y0 /= ymag;
- y1 /= ymag;
- y2 /= ymag;
- }
+ return this._renderer.createCamera();
+ };
- return {
- x: [x0, x1, x2],
- y: [y0, y1, y2],
- z: [z0, z1, z2]
- };
- }
+ /**
+ * 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);
+ };
/**
- * 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
+ * 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();
+ * }
+ *
+ *
+ *
+ *
+ *
+ * // 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;
+ * }
+ * }
+ *
+ *
*/
- _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
+ p5.Camera = Camera;
- // 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;
- }
+ RendererGL.prototype.camera = function(...args) {
+ this.states.curCamera.camera(...args);
+ };
- // 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;
+ RendererGL.prototype.perspective = function(...args) {
+ this.states.curCamera.perspective(...args);
+ };
- // 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;
+ RendererGL.prototype.linePerspective = function(enable) {
+ if (enable !== undefined) {
+ // 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;
}
+ };
- // 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);
+ RendererGL.prototype.ortho = function(...args) {
+ this.states.curCamera.ortho(...args);
+ };
- // 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);
+ RendererGL.prototype.frustum = function(...args) {
+ this.states.curCamera.frustum(...args);
+ };
- // 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;
- }
+ RendererGL.prototype.createCamera = function() {
+ // compute default camera settings, then set a default camera
+ const _cam = new Camera(this);
+ _cam._computeCameraDefaultSettings();
+ _cam._setDefaultCamera();
- // 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.
+ return _cam;
+ };
- // 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);
+ RendererGL.prototype.setCamera = function(cam) {
+ this.states.setValue('curCamera', cam);
- // 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
- );
- }
+ // 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);
+ };
/**
- * Returns true if camera is currently attached to renderer.
- * @private
+ * 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);
+ * }
+ *
+ *
+ *
+ *
+ *
+ * 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);
+ * }
+ *
+ *
*/
- _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.
+ * The camera’s y-coordinate.
*
- * `camera()` allows objects to be viewed from different angles. It has nine
- * parameters that are all optional.
+ * By default, the camera’s y-coordinate is set to 0 in "world" space.
*
- * 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)`.
+ * @property {Number} eyeY
+ * @for p5.Camera
+ * @readonly
*
- * 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)`.
+ * @example
+ *
+ *
+ * let cam;
+ * let font;
*
- * 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)`.
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
*
- * Note: `camera()` can only be used in WebGL mode.
+ * // Create a p5.Camera object.
+ * cam = createCamera();
*
- * @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
+ * // 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
*
*
- * function setup() {
+ * 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);
*
- * describe('A white cube on a gray background.');
+ * // 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);
*
- * // Move the camera to the top-right.
- * camera(200, -400, 800);
+ * // 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
*
*
- * function setup() {
+ * 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);
*
- * describe('A white cube apperas to sway left and right on a gray background.');
+ * // 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);
*
- * // Calculate the camera's x-coordinate.
- * let x = 400 * cos(frameCount * 0.01);
+ * // Style the box.
+ * fill(255);
*
- * // Orbit the camera around the box.
- * camera(x, -400, 800);
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * // Draw the box.
- * box();
+ * // Display the value of eyeZ, rounded to the nearest integer.
+ * text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45);
* }
*
*
*
*
*
- * // Adjust the range sliders to change the camera's position.
- *
- * let xSlider;
- * let ySlider;
- * let zSlider;
+ * let cam;
+ * let font;
*
- * function setup() {
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
* 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);
+ * // 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 drawn against a gray background. Three range sliders appear beneath the image. The camera position changes when the user moves the sliders.'
+ * '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);
*
- * // 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);
+ * // 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);
* }
*
*
*/
- 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.
+ * The x-coordinate of the place where the camera looks.
*
- * Note: `perspective()` can only be used in WebGL mode.
+ * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
+ * `myCamera.centerX` is 0.
*
- * @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
+ * @property {Number} centerX
+ * @for p5.Camera
+ * @readonly
*
* @example
*
*
- * // Double-click to squeeze the box.
- *
- * let isSqueezed = false;
+ * let cam;
+ * let font;
*
- * function setup() {
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
* createCanvas(100, 100, WEBGL);
*
- * describe('A white rectangular prism on a gray background. The box appears to become thinner when the user double-clicks.');
+ * // 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);
*
- * // 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);
- * }
+ * // Style the box.
+ * fill(255);
*
* // Draw the box.
* box();
- * }
*
- * // Change the camera's perspective when the user double-clicks.
- * function doubleClicked() {
- * isSqueezed = true;
+ * // 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);
* }
*
*
*
*
*
- * function setup() {
+ * 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);
*
- * describe('A white rectangular prism on a gray background. The prism moves away from the camera until it disappears.');
- * }
+ * // Create a p5.Camera object.
+ * cam = createCamera();
*
- * function draw() {
- * background(200);
+ * // Set the camera
+ * setCamera(cam);
*
* // Place the camera at the top-right.
- * camera(400, -400, 800);
+ * cam.setPosition(100, -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);
+ * // Point the camera at (10, 20, -30).
+ * cam.lookAt(10, 20, -30);
*
- * // Move the origin away from the camera.
- * let x = -frameCount;
- * let y = frameCount;
- * let z = -2 * frameCount;
- * translate(x, y, z);
+ * 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();
- * }
- *
- *
- */
- 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.
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * `linePerspective()` can override the default line drawing mode.
+ * // Calculate the new x-coordinate.
+ * let x = 25 * sin(frameCount * 0.01) + 10;
*
- * 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.
+ * // Point the camera.
+ * cam.lookAt(x, 20, -30);
*
- * Calling `linePerspective()` without passing an argument returns `true` if
- * it's enabled and `false` if not.
+ * // 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.
*
- * Note: `linePerspective()` can only be used in WebGL mode.
+ * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
+ * `myCamera.centerY` is 0.
*
- * @method linePerspective
- * @for p5
- * @param {Boolean} enable whether to enable line perspective.
+ * @property {Number} centerY
+ * @for p5.Camera
+ * @readonly
*
* @example
*
*
- * // Double-click the canvas to toggle the line perspective.
+ * let cam;
+ * let font;
*
- * function setup() {
+ * 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 with black edges on a gray background. Its edges toggle between thick and thin when the user double-clicks.'
+ * 'A white cube on a gray background. The text "centerY: 20" is written in black beneath it.'
* );
* }
*
* function draw() {
* background(200);
*
- * // Translate the origin toward the camera.
- * translate(-10, 10, 600);
+ * // Style the box.
+ * fill(255);
*
- * // Rotate the coordinate system.
- * rotateY(-0.1);
- * rotateX(-0.1);
+ * // Draw the box.
+ * box();
*
- * // Draw the row of boxes.
- * for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -40);
- * box(10);
- * }
- * }
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * // Toggle the line perspective when the user double-clicks.
- * function doubleClicked() {
- * let isEnabled = linePerspective();
- * linePerspective(!isEnabled);
+ * // Display the value of centerY, rounded to the nearest integer.
+ * text(`centerY: ${round(cam.centerY)}`, 0, 45);
* }
*
*
*
*
*
- * // Double-click the canvas to toggle the line perspective.
+ * let cam;
+ * let font;
*
- * function setup() {
+ * 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 row of cubes with black edges on a gray background. Their edges toggle between thick and thin when the user double-clicks.'
+ * '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);
*
- * // Use an orthographic projection.
- * ortho();
+ * // Style the box.
+ * fill(255);
*
- * // Translate the origin toward the camera.
- * translate(-10, 10, 600);
+ * // Draw the box.
+ * box();
*
- * // Rotate the coordinate system.
- * rotateY(-0.1);
- * rotateX(-0.1);
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * // Draw the row of boxes.
- * for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -40);
- * box(10);
- * }
- * }
+ * // Calculate the new y-coordinate.
+ * let y = 25 * sin(frameCount * 0.01) + 20;
*
- * // Toggle the line perspective when the user double-clicks.
- * function doubleClicked() {
- * let isEnabled = linePerspective();
- * linePerspective(!isEnabled);
+ * // 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);
* }
*
*
*/
- /**
- * @method linePerspective
- * @return {boolean} whether line perspective is enabled.
- */
- fn.linePerspective = function (enable) {
- // p5._validateParameters('linePerspective', arguments);
- if (!(this._renderer instanceof RendererGL)) {
- 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.
+ * The y-coordinate of the place where the camera looks.
*
- * Note: `ortho()` can only be used in WebGL mode.
+ * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
+ * `myCamera.centerZ` is 0.
*
- * @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
+ * @property {Number} centerZ
+ * @for p5.Camera
+ * @readonly
*
* @example
*
*
- * function setup() {
+ * 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);
*
- * describe('A row of tiny, white cubes on a gray background. All the cubes appear the same size.');
+ * // 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);
*
- * // Apply an orthographic projection.
- * ortho();
+ * // Style the box.
+ * fill(255);
*
- * // Translate the origin toward the camera.
- * translate(-10, 10, 600);
+ * // Draw the box.
+ * box();
*
- * // Rotate the coordinate system.
- * rotateY(-0.1);
- * rotateX(-0.1);
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * // Draw the row of boxes.
- * for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -40);
- * box(10);
- * }
+ * // Display the value of centerZ, rounded to the nearest integer.
+ * text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
* }
*
*
*
*
*
- * function setup() {
+ * 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);
*
- * describe('A white cube on a gray background.');
+ * // 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);
*
- * // 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);
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * // Translate the origin toward the camera.
- * translate(-10, 10, 600);
+ * // Calculate the new z-coordinate.
+ * let z = 25 * sin(frameCount * 0.01) - 30;
*
- * // Rotate the coordinate system.
- * rotateY(-0.1);
- * rotateX(-0.1);
+ * // Point the camera.
+ * cam.lookAt(10, 20, z);
*
- * // Draw the row of boxes.
- * for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -40);
- * box(10);
- * }
+ * // Display the value of centerZ, rounded to the nearest integer.
+ * text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
* }
*
*
*/
- 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.
+ * The x-component of the camera's "up" vector.
*
- * Note: `frustum()` can only be used in WebGL mode.
+ * 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.
*
- * @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
+ * @property {Number} upX
+ * @for p5.Camera
+ * @readonly
*
* @example
*
*
- * function setup() {
+ * 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);
*
- * describe('A row of white cubes on a gray background.');
+ * // 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);
*
- * // Apply the default frustum projection.
- * frustum();
+ * // Style the box.
+ * fill(255);
*
- * // Translate the origin toward the camera.
- * translate(-10, 10, 600);
+ * // Draw the box.
+ * box();
*
- * // Rotate the coordinate system.
- * rotateY(-0.1);
- * rotateX(-0.1);
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * // Draw the row of boxes.
- * for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -40);
- * box(10);
- * }
+ * // Display the value of upX, rounded to the nearest tenth.
+ * text(`upX: ${round(cam.upX, 1)}`, 0, 45);
* }
*
*
*
*
*
- * function setup() {
+ * 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);
- * describe('A white cube on a gray background.');
+ *
+ * // 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);
*
- * // 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);
+ * // Style the box.
+ * fill(255);
*
- * // Translate the origin toward the camera.
- * translate(-10, 10, 600);
+ * // Draw the box.
+ * box();
*
- * // Rotate the coordinate system.
- * rotateY(-0.1);
- * rotateX(-0.1);
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
*
- * // Draw the row of boxes.
- * for (let i = 0; i < 6; i += 1) {
- * translate(0, 0, -40);
- * box(10);
- * }
+ * // 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);
* }
*
*
*/
- 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.
+ * The y-component of the camera's "up" vector.
*
- * Note: `createCamera()` can only be used in WebGL mode.
+ * 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.
*
- * @method createCamera
- * @return {p5.Camera} the new camera.
- * @for p5
+ * @property {Number} upY
+ * @for p5.Camera
+ * @readonly
*
* @example
*
*
- * // Double-click to toggle between cameras.
- *
- * let cam1;
- * let cam2;
- * let usingCam1 = true;
+ * let cam;
+ * let font;
*
- * function setup() {
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
* createCanvas(100, 100, WEBGL);
*
- * // Create the first camera.
- * // Keep its default settings.
- * cam1 = createCamera();
+ * // Create a p5.Camera object.
+ * cam = 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 camera
+ * setCamera(cam);
*
- * // Set the current camera to cam1.
- * setCamera(cam1);
+ * // 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 camera toggles between frontal and aerial views when the user double-clicks.');
+ * describe(
+ * 'A white cube on a gray background. The text "upY: 1" is written in black beneath it.'
+ * );
* }
*
* 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.
+ * background(200);
*
- * `setCamera()` allows for switching between multiple cameras created with
- * createCamera().
+ * // Style the box.
+ * fill(255);
*
- * Note: `setCamera()` can only be used in WebGL mode.
+ * // Draw the box.
+ * box();
*
- * @method setCamera
- * @param {p5.Camera} cam camera that should be made active.
- * @for p5
+ * // 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
*
*
- * // Double-click to toggle between cameras.
- *
- * let cam1;
- * let cam2;
- * let usingCam1 = true;
+ * let cam;
+ * let font;
*
- * function setup() {
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
* createCanvas(100, 100, WEBGL);
*
- * // Create the first camera.
- * // Keep its default settings.
- * cam1 = createCamera();
+ * // Create a p5.Camera object.
+ * cam = 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 camera
+ * setCamera(cam);
*
- * // Set the current camera to cam1.
- * setCamera(cam1);
+ * // 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 camera toggles between frontal and aerial views when the user double-clicks.');
+ * 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();
- * }
*
- * // Toggle the current camera when the user double-clicks.
- * function doubleClicked() {
- * if (usingCam1 === true) {
- * setCamera(cam2);
- * usingCam1 = false;
- * } else {
- * setCamera(cam1);
- * usingCam1 = true;
- * }
+ * // 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);
* }
*
*
*/
- 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().
+ * The z-component of the camera's "up" vector.
*
- * 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.
+ * 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.
*
- * @class p5.Camera
- * @constructor
- * @param {rendererGL} rendererGL instance of WebGL renderer
+ * @property {Number} upZ
+ * @for p5.Camera
+ * @readonly
*
* @example
*
*
* let cam;
- * let delta = 0.001;
+ * let font;
*
- * function setup() {
+ * 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.
@@ -3865,130 +3915,90 @@ function camera(p5, fn){
* // 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);
+ * // 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 goes in and out of view as the camera pans left and right.'
+ * 'A white cube on a gray background. The text "upZ: 0" is written in black beneath it.'
* );
* }
*
* 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;
- * }
+ * // 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);
* }
*
*
*
*
*
- * // Double-click to toggle between cameras.
- *
- * let cam1;
- * let cam2;
- * let isDefaultCamera = true;
+ * let cam;
+ * let font;
*
- * function setup() {
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
* createCanvas(100, 100, WEBGL);
*
- * // Create the first camera.
- * // Keep its default settings.
- * cam1 = createCamera();
+ * // Create a p5.Camera object.
+ * cam = 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 camera
+ * setCamera(cam);
*
- * // Set the current camera to cam1.
- * setCamera(cam1);
+ * // 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 camera toggles between frontal and aerial views when the user double-clicks.'
+ * '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();
- * }
*
- * // Toggle the current camera when the user double-clicks.
- * function doubleClicked() {
- * if (isDefaultCamera === true) {
- * setCamera(cam2);
- * isDefaultCamera = false;
- * } else {
- * setCamera(cam1);
- * isDefaultCamera = true;
- * }
+ * // 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);
* }
*
*
*/
- p5.Camera = Camera;
-
- RendererGL.prototype.camera = function(...args) {
- this.states.curCamera.camera(...args);
- };
-
- RendererGL.prototype.perspective = function(...args) {
- this.states.curCamera.perspective(...args);
- };
-
- RendererGL.prototype.linePerspective = function(enable) {
- if (enable !== undefined) {
- // 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;
- }
- };
-
- RendererGL.prototype.ortho = function(...args) {
- this.states.curCamera.ortho(...args);
- };
-
- RendererGL.prototype.frustum = function(...args) {
- this.states.curCamera.frustum(...args);
- };
-
- RendererGL.prototype.createCamera = function() {
- // compute default camera settings, then set a default camera
- const _cam = new Camera(this);
- _cam._computeCameraDefaultSettings();
- _cam._setDefaultCamera();
-
- return _cam;
- };
-
- RendererGL.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);
- };
}
export default camera;
diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js
index ad481fb586..a3d6875068 100644
--- a/src/webgl/p5.Framebuffer.js
+++ b/src/webgl/p5.Framebuffer.js
@@ -1107,7 +1107,7 @@ class Framebuffer {
/**
* Ensure all readable textures are up-to-date.
* @private
- * @property {'colorTexutre'|'depthTexture'} property The property to update
+ * @param {'colorTexutre'|'depthTexture'} property The property to update
*/
_update(property) {
if (this.dirty[property] && this.antialias) {
@@ -1308,8 +1308,6 @@ class Framebuffer {
* `myBuffer.loadPixels()` must be called before reading from or writing to
* myBuffer.pixels.
*
- * @method loadPixels
- *
* @example
*
*
@@ -1887,6 +1885,20 @@ function framebuffer(p5, fn){
*
*
*/
+
+ /**
+ * The current width of the framebuffer.
+ *
+ * @property {Number} width
+ * @for p5.Framebuffer
+ */
+
+ /**
+ * The current width of the framebuffer.
+ *
+ * @property {Number} height
+ * @for p5.Framebuffer
+ */
}
export default framebuffer;
diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js
index 1efcc3d991..a5691e7854 100644
--- a/src/webgl/p5.Geometry.js
+++ b/src/webgl/p5.Geometry.js
@@ -432,8 +432,8 @@ class Geometry {
* @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. Options can include a boolean `binary` property, which
- * controls whether or not a binary .stl file is saved. It defaults to false.
+ * @param {Object} [options] Optional settings.
+ * @param {Boolean} [options.binary=false] Whether or not a binary .stl file is saved.
* @example
*
*
@@ -1953,7 +1953,7 @@ function geometry(p5, fn){
* @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.
+ * @param {Function} [callback] function to call once the geometry is created.
*
* @example
*
@@ -2550,6 +2550,13 @@ function geometry(p5, fn){
*
*
*/
+
+ /**
+ * A unique identifier for this geometry. The renderer will use this to cache resources.
+ *
+ * @property {String} gid
+ * @for p5.Geometry
+ */
}
export default geometry;
diff --git a/src/webgl/p5.Quat.js b/src/webgl/p5.Quat.js
index 3ad8681c5b..d10f509b94 100644
--- a/src/webgl/p5.Quat.js
+++ b/src/webgl/p5.Quat.js
@@ -55,6 +55,7 @@ class Quat {
* 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) {
diff --git a/src/webgl/text.js b/src/webgl/text.js
index 3358466a8c..a69ef3736f 100644
--- a/src/webgl/text.js
+++ b/src/webgl/text.js
@@ -132,6 +132,7 @@ function text(p5, fn) {
/**
* @function setPixel
+ * @private
* @param {Object} imageInfo
* @param {Number} r
* @param {Number} g
@@ -230,6 +231,7 @@ function text(p5, fn) {
/**
* @function push
+ * @private
* @param {Number[]} xs the x positions of points in the curve
* @param {Number[]} ys the y positions of points in the curve
* @param {Object} v the curve information
@@ -242,6 +244,7 @@ function text(p5, fn) {
/**
* @function minMax
+ * @private
* @param {Number[]} rg the list of values to compare
* @param {Number} min the initial minimum value
* @param {Number} max the initial maximum value
@@ -291,6 +294,7 @@ function text(p5, fn) {
/**
* @function clamp
+ * @private
* @param {Number} v the value to clamp
* @param {Number} min the minimum value
* @param {Number} max the maxmimum value
@@ -305,6 +309,7 @@ function text(p5, fn) {
/**
* @function byte
+ * @private
* @param {Number} v the value to scale
*
* converts a floating-point number in the range 0-1 to a byte 0-255
@@ -440,6 +445,7 @@ function text(p5, fn) {
/**
* @function cubicToQuadratics
+ * @private
* @param {Number} x0
* @param {Number} y0
* @param {Number} cx0
@@ -508,6 +514,7 @@ function text(p5, fn) {
/**
* @function pushLine
+ * @private
* @param {Number} x0
* @param {Number} y0
* @param {Number} x1
@@ -523,6 +530,7 @@ function text(p5, fn) {
/**
* @function samePoint
+ * @private
* @param {Number} x0
* @param {Number} y0
* @param {Number} x1
@@ -608,9 +616,10 @@ function text(p5, fn) {
/**
* @function layout
+ * @private
* @param {Number[][]} dim
- * @param {ImageInfo[]} dimImageInfos
- * @param {ImageInfo[]} cellImageInfos
+ * @param {ImageInfos} dimImageInfos
+ * @param {ImageInfos} cellImageInfos
* @return {Object}
*
* lays out the curves in a dimension (row or col) into two
diff --git a/test/types/basic.ts b/test/types/basic.ts
new file mode 100644
index 0000000000..37bc4f9f33
--- /dev/null
+++ b/test/types/basic.ts
@@ -0,0 +1,73 @@
+// Modified from https://openprocessing.org/sketch/2500100
+
+import '../../types/global'
+
+p5.disableFriendlyErrors = true
+
+let geom: p5.Geometry
+let geom2: p5.Geometry
+
+function setup() {
+ createCanvas(windowWidth, windowHeight, WEBGL)
+ geom2 = new p5.Geometry(1, 1, function() {
+ this.vertices.push(createVector(0, 0, 0))
+ this.vertices.push(createVector(1, 0, 0))
+ this.vertices.push(createVector(1, 1, 0))
+ this.faces.push([0, 1, 2])
+ this.computeNormals()
+ })
+}
+
+function regenerate() {
+ if (geom) {
+ freeGeometry(geom)
+ }
+ geom = buildGeometry(() => {
+ let n = round(random(5, 20))
+ for (let i = 0; i <= n; i++) {
+ push()
+ translate(
+ random(-1, 1)*width*0.05,
+ map(i, 0, n, height*0.4, -height*0.4) + random(-1,1)*height*0.05
+ )
+ rotateX(PI/2 + random(-1, 1) * PI * 0.15)
+ rotateZ(random(-1, 1) * PI * 0.15)
+ torus(
+ random(0.1, 0.3) * width,
+ random(0.01, 0.05) * width,
+ 50,
+ 30
+ )
+ pop()
+ }
+ })
+ geom.clearColors()
+}
+
+let lastScene = -1
+function draw() {
+ const period = 8000
+
+ const ms = millis()
+ const scene = floor(ms / period)
+ if (scene !== lastScene) {
+ regenerate()
+ lastScene = scene
+ }
+
+ const t = (ms % period)/period
+ background(0)
+ orbitControl()
+ const s = map(t, 0, 0.2, 0, 1, true) * map(t, 0.8, 1, 1, 0, true)
+ directionalLight(s*255, s*255, s*255, -0.4, 0, 1)
+ directionalLight(s*255, s*255, s*255, 0.4, 0, 1)
+ directionalLight(s*255, s*255, s*255, 0, -0.4, 1)
+ directionalLight(s*255, s*255, s*255, 0, 0.4, 1)
+ noStroke()
+ fill(100)
+ specularMaterial(255)
+ shininess(400)
+ scale(0.8)
+ rotateY(millis() * 0.0001)
+ model(geom)
+}
diff --git a/test/types/generics.ts b/test/types/generics.ts
new file mode 100644
index 0000000000..a86846e3f4
--- /dev/null
+++ b/test/types/generics.ts
@@ -0,0 +1,31 @@
+import '../../types/global'
+
+function setup() {
+ noCanvas()
+
+ const messages = [
+ { content: 'Hello, world!' },
+ { content: "How's it going?" },
+ ]
+ const message = random(messages)
+
+ // The types should fail if the result of random() is any
+ logMessage(message);
+}
+
+// From: https://stackoverflow.com/a/50375286/62076
+type UnionToIntersection =
+ (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
+
+// If T is `any` a union of both side of the condition is returned.
+type UnionForAny = T extends never ? 'A' : 'B'
+
+// Returns true if type is any, or false for any other type.
+type IsStrictlyAny =
+ UnionToIntersection> extends never ? true : false
+
+type NotAny = IsStrictlyAny extends true ? never : T
+
+function logMessage(message: NotAny<{ content: string }>) {
+ console.log(message)
+}
diff --git a/test/types/instance.ts b/test/types/instance.ts
new file mode 100644
index 0000000000..cd643d7ef9
--- /dev/null
+++ b/test/types/instance.ts
@@ -0,0 +1,20 @@
+import P5 from '../../types/p5'
+
+P5.disableFriendlyErrors = true
+
+const sketch = new P5((p) => {
+ let g: P5.Graphics
+ p.setup = function() {
+ p.createCanvas(200, 200)
+ g = p.createGraphics(200, 200)
+ }
+
+ p.mouseMoved = function() {
+ g.circle(p.mouseX, p.mouseY, 20);
+ }
+
+ p.draw = function() {
+ p.clear()
+ p.image(g, 0, 0)
+ }
+})
diff --git a/test/types/typography.ts b/test/types/typography.ts
new file mode 100644
index 0000000000..972e43d0df
--- /dev/null
+++ b/test/types/typography.ts
@@ -0,0 +1,73 @@
+// From https://openprocessing.org/sketch/2523015
+import '../../types/global'
+
+let font: p5.Font
+const txt = 'p5.js'
+
+async function setup() {
+ createCanvas(windowWidth, windowHeight);
+ font = await loadFont('https://fonts.gstatic.com/s/sniglet/v17/cIf4MaFLtkE3UjaJ_ImHRGEsnIJkWL4.ttf')
+}
+
+let prevTxt = ''
+let prevTxtTime = 0
+function draw() {
+ if (txt !== prevTxt) {
+ prevTxt = txt
+ prevTxtTime = millis()
+ }
+ const progress = pow(map(millis(), prevTxtTime, prevTxtTime + 2000, 0, 1, true), 0.5)
+
+ const contours = font.textToContours(txt, 0, 0, { sampleFactor: 1 })
+
+ background(0)
+ textAlign(CENTER, CENTER)
+ textSize(120)
+ textFont(font)
+ translate(width/2, height/2)
+ scale(min(width, height)/300)
+ const w = max(10, fontWidth(txt))
+ scale((width * 0.2) / w)
+
+ beginShape()
+ for (const contour of contours) {
+ beginContour()
+ for (const pt of contour) {
+ vertex(pt.x, pt.y)
+ }
+ endContour(CLOSE)
+ }
+ endShape()
+
+ push()
+ strokeWeight(0.5)
+ stroke('rgb(255,127,228)')
+ noFill()
+
+ beginShape(LINES)
+ for (const contour of contours) {
+ const pts = contour.map((v) => createVector(v.x, v.y))
+ if (pts[0].dist(pts.at(-1)) === 0) pts.pop()
+ const dists = pts.map((pt, i) => max(1e-6, pt.dist(pts[(i+1)%pts.length])))
+
+ let tangents = pts.map((v, i) => pts[(i+1)%pts.length].copy().sub(v).div(dists[i]))
+ for (let it = 0; it < 2; it++) {
+ tangents = tangents.map(
+ (tangent, i) =>
+ tangent.copy()
+ .add(tangents[(i-1+pts.length)%pts.length])
+ .add(tangents[(i+1)%pts.length])
+ .mult(1/3)
+ )
+ }
+
+ const ks = tangents.map((t, i) => tangents[(i+1)%pts.length].copy().sub(t))
+
+ pts.forEach((pt, i) => {
+ vertex(pt.x, pt.y)
+ vertex(pt.x + ks[i].x * -120 * progress, pt.y + ks[i].y * -120 * progress)
+ })
+ }
+ endShape()
+ pop()
+}
diff --git a/test/types/webgl-pixels.ts b/test/types/webgl-pixels.ts
new file mode 100644
index 0000000000..8c60186372
--- /dev/null
+++ b/test/types/webgl-pixels.ts
@@ -0,0 +1,348 @@
+// From https://openprocessing.org/sketch/2308573
+
+import '../../types/global'
+// Fake matter.js import
+declare const Matter: any
+
+let engine: any
+let blobs: Blob[] = []
+let metaballShader: p5.Shader
+let spheremap: p5.Image
+let renderer: p5.RendererGL
+
+async function setup() {
+ spheremap = await loadImage('https://deckard.openprocessing.org/user67809/visual2181338/h987a85d77bacbc3b232fb87ce6fe440a/dusseldorf_bridge.jpg')
+ renderer = createCanvas(600, 600, WEBGL)
+ metaballShader = createShader(vert, frag)
+ setupScene()
+ blobs.push(new Blob(random(-1,1)*100, 50, 100, '#f3e17e'))
+ blobs.push(new Blob(random(-1,1)*100, -150, 100, '#dd483c'))
+ blobs.push(new Blob(random(-1,1)*100, -350, 50, '#4b8a5f'))
+ blobs.push(new Blob(random(-1,1)*100, -550, 50, '#0d150b'))
+}
+
+function setupScene() {
+ engine = Matter.Engine.create()
+
+ const ground = Matter.Bodies.rectangle(0, height / 2 + 30, width, 60, {
+ isStatic: true,
+ })
+ const wallLeft = Matter.Bodies.rectangle(-width/2 - 30, 0, 60, 3 * height, {
+ isStatic: true,
+ })
+ const wallRight = Matter.Bodies.rectangle(width/2 + 30, 0, 60, 3 * height, {
+ isStatic: true,
+ })
+ Matter.World.add(engine.world, [ground, wallLeft, wallRight])
+}
+
+function draw() {
+ background('#faf8e2')
+ // translate(width/2, height/2)
+
+ for (const blob of blobs) {
+ blob.update()
+ }
+ Matter.Engine.update(engine, 1000 / 60)
+
+ for (const blob of blobs) {
+ blob.drawBlob()
+ }
+}
+
+const BLOB_NODE_SIZE = 20
+const BLOB_NODE_R = 15
+const BLOB_NODE_AREA = Math.PI * BLOB_NODE_SIZE * BLOB_NODE_SIZE
+
+class Blob {
+ c: p5.Color
+ nodes: any[]
+ springs: any[]
+ tex: p5.Image
+
+ constructor(x, y, r, c) {
+ this.nodes = []
+ this.springs = []
+ this.c = color(c)
+ this.tex = createImage(20, 20)
+ this.tex.loadPixels()
+ for (let i = 0; i < this.tex.pixels.length; i++) {
+ this.tex.pixels[i] = 255
+ }
+ // @ts-ignore
+ renderer.getTexture(this.tex).setInterpolation(NEAREST, NEAREST)
+
+ const a = PI * r * r
+ const numBlobs = ceil(a / BLOB_NODE_AREA)
+
+ while (this.nodes.length < numBlobs) {
+ const rx = random(-r, r)
+ const ry = random(-r, r)
+ if (Math.hypot(rx, ry) > r) continue
+
+ const vert = Matter.Bodies.circle(x + rx, y + ry, BLOB_NODE_R, { inertia: Infinity, friction: 0.015 })
+ this.nodes.push(vert)
+ }
+
+ Matter.World.add(engine.world, this.nodes)
+ }
+
+ bin(x: number, y: number) {
+ return [round(x/80), round(y/80)]
+ }
+
+ nodeBin(node) {
+ return this.bin(node.position.x, node.position.y)
+ }
+
+ adjacentBins(node) {
+ const [x, y] = this.nodeBin(node)
+ const bins: [number, number][] = []
+ for (const dx of [-1, 0, 1]) {
+ for (const dy of [-1, 0, 1]) {
+ bins.push([x + dx, y + dy])
+ }
+ }
+ return bins
+ }
+
+ binKey(bin) {
+ return bin.join(',')
+ }
+
+ binnedNodes() {
+ const bins = {}
+ for (const node of this.nodes) {
+ const binKey = this.binKey(this.nodeBin(node))
+ if (!bins[binKey]) {
+ bins[binKey] = []
+ }
+ bins[binKey].push(node)
+ }
+ return bins
+ }
+
+ update() {
+ Matter.World.remove(engine.world, this.springs)
+ this.springs = []
+ const bins = this.binnedNodes()
+ for (const node of this.nodes) {
+ const binsToCheck = this.adjacentBins(node)
+ for (const bin of binsToCheck) {
+ const key = this.binKey(bin)
+ if (!bins[key]) continue
+ for (const other of bins[key]) {
+ if (other === node) continue
+ this.springs.push(Matter.Constraint.create({
+ bodyA: node,
+ pointA: { x: 0, y: 0 },
+ bodyB: other,
+ pointB: { x: 0, y: 0 },
+ stiffness: map(
+ Math.hypot(node.position.x - other.position.x, node.position.y - other.position.y),
+ 0, 12*BLOB_NODE_SIZE,
+ 0.02, 0.03,
+ true
+ ),
+ damping: 0.001,
+ // length: 0,
+ length: max(
+ 2 * BLOB_NODE_SIZE,
+ Math.hypot(node.position.x - other.position.x, node.position.y - other.position.y) * 0.975
+ ),
+ }))
+ }
+ }
+ }
+ Matter.World.add(engine.world, this.springs)
+ }
+
+ drawBlob() {
+ const minX = Math.min(...this.nodes.map((n) => n.position.x)) - 4 * BLOB_NODE_SIZE
+ const maxX = Math.max(...this.nodes.map((n) => n.position.x)) + 4 * BLOB_NODE_SIZE
+ const minY = Math.min(...this.nodes.map((n) => n.position.y)) - 4 * BLOB_NODE_SIZE
+ const maxY = Math.max(...this.nodes.map((n) => n.position.y)) + 4 * BLOB_NODE_SIZE
+ const x = (maxX + minX)/2
+ const y = (maxY + minY)/2
+ const w = maxX - minX
+ const h = maxY - minY
+
+ this.nodes.forEach((node, i) => {
+ this.tex.pixels[i * 4 + 0] = map(node.position.x, minX, maxX, 0, 255, true)
+ this.tex.pixels[i * 4 + 1] = map(node.position.y, minY, maxY, 0, 255, true)
+ })
+ this.tex.updatePixels()
+
+ push()
+ translate(x, y)
+ noStroke()
+ shader(metaballShader)
+ metaballShader.setUniform('bbox', [minX, minY, maxX, maxY])
+ metaballShader.setUniform('k', BLOB_NODE_SIZE * 3)
+ metaballShader.setUniform('numNodes', this.nodes.length)
+ metaballShader.setUniform('data', this.tex)
+ metaballShader.setUniform('r', BLOB_NODE_R)
+ // TODO: make this a public API
+ // @ts-ignore
+ metaballShader.setUniform('c', this.c.array())
+ metaballShader.setUniform('spheremap', spheremap)
+ plane(w, h)
+ pop()
+ }
+
+ draw2D() {
+ fill(this.c)
+ stroke(this.c)
+ strokeWeight(2 * BLOB_NODE_R)
+ strokeJoin(ROUND)
+ const hull = convexHull(this.nodes.map(n => n.position))
+ beginShape()
+ for (const { x, y } of hull) vertex(x, y)
+ endShape(CLOSE)
+
+ noStroke()
+ fill(0)
+ for (const node of this.nodes) {
+ circle(node.position.x, node.position.y, BLOB_NODE_R * 2)
+ }
+ }
+}
+
+let vert = `#version 300 es
+precision highp float;
+
+in vec3 aPosition;
+in vec2 aTexCoord;
+
+uniform mat4 uModelViewMatrix;
+uniform mat4 uProjectionMatrix;
+
+out vec2 vTexCoord;
+
+void main() {
+ // Apply the camera transform
+ vec4 viewModelPosition =
+ uModelViewMatrix *
+ vec4(aPosition, 1.0);
+
+ // Tell WebGL where the vertex goes
+ gl_Position =
+ uProjectionMatrix *
+ viewModelPosition;
+
+ // Pass along data to the fragment shader
+ vTexCoord = aTexCoord;
+}`
+
+let frag = `#version 300 es
+precision highp float;
+
+in vec2 vTexCoord;
+out vec4 fragColor;
+
+uniform sampler2D data;
+uniform sampler2D spheremap;
+uniform vec4 bbox;
+uniform int numNodes;
+uniform float k;
+uniform float r;
+uniform vec4 c;
+
+float opSmoothUnion( float d1, float d2, float k )
+{
+ float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
+ return mix( d2, d1, h ) - k*h*(1.0-h);
+}
+
+vec2 nodeCoord(int i) {
+ float x = fract(float(i)/20.) + 1./40.;
+ float y = floor(float(i)/20.)/20. + 1./40.;
+ vec2 pos = texture(data, vec2(x, y)).xy;
+ return mix(bbox.xy, bbox.zw, pos);
+}
+
+void main() {
+ vec2 coord = mix(bbox.xy, bbox.zw, vTexCoord);
+
+ float dist = 100000.;
+ float avg = 0.;
+ float total = 0.;
+ for (int i = 0; i < 400; i++) {
+ if (i >= numNodes) break;
+
+ float dist2 = length(coord - nodeCoord(i)) - r;
+ avg += pow(dist2, 2.);
+ total += 1.;
+ dist = opSmoothUnion(
+ dist,
+ dist2,
+ k
+ );
+ // dist = min(dist, dist2);
+ }
+ avg /= total;
+ // vec3 pos = vec3(coord, avg * 0.05);
+ vec3 pos = vec3(coord, -40.*smoothstep(0., 2., pow(-dist, .1)));
+
+ vec3 normal = -normalize(cross(dFdx(pos), dFdy(pos)));
+
+ vec3 fromCam = normalize(pos - vec3(0., 0., -800.0));
+ vec3 n = reflect(
+ fromCam,
+ normal
+ );
+ float phi = acos( n.y );
+ float theta = 0.0;
+ theta = acos(n.x / sin(phi));
+ float sinTheta = n.z / sin(phi);
+ if (sinTheta < 0.0) {
+ // Turn it into -theta, but in the 0-2PI range
+ theta = 2.0 * 3.14159 - theta;
+ }
+ theta = theta / (2.0 * 3.14159);
+ phi = phi / 3.14159 ;
+ vec2 angles = vec2( fract(theta + 0.25), 1.0 - phi );
+
+ vec3 lightDir = normalize(vec3(-0.3, 0.9, 0.));
+ float l = 0.15 * max(0., dot(lightDir, normal)) + 0.85;
+ vec3 outColor = c.xyz * l + pow(texture(spheremap, angles).xyz, vec3(4.));
+ // outColor = vec3(smoothstep(0., 2., pow(-dist, .15)));
+ // outColor = normal;
+
+ fragColor = vec4(outColor, 1.) * (1. - smoothstep(0., 0.01, dist));
+}`
+
+const comparison = (a: p5.Vector, b: p5.Vector) => {
+ return a.x == b.x ? a.y - b.y : a.x - b.x
+}
+
+const cross = (a: p5.Vector, b: p5.Vector, o: p5.Vector) => {
+ return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
+}
+
+function convexHull(points: p5.Vector[]) {
+ points.sort(comparison)
+ const L: p5.Vector[] = []
+ for (let i = 0; i < points.length; i++) {
+ while (
+ L.length >= 2 &&
+ cross(L[L.length - 2], L[L.length - 1], points[i]) <= 0
+ ) {
+ L.pop()
+ }
+ L.push(points[i])
+ }
+ const U: p5.Vector[] = []
+ for (let i = points.length - 1; i >= 0; i--) {
+ while (
+ U.length >= 2 &&
+ cross(U[U.length - 2], U[U.length - 1], points[i]) <= 0
+ ) {
+ U.pop()
+ }
+ U.push(points[i])
+ }
+ L.pop()
+ U.pop()
+ return L.concat(U)
+}
diff --git a/test/unit/types/generate-types.js b/test/unit/types/generate-types.js
deleted file mode 100644
index 47fa5b1f81..0000000000
--- a/test/unit/types/generate-types.js
+++ /dev/null
@@ -1,423 +0,0 @@
-import { suite, test, expect } from 'vitest';
-import {
- normalizeClassName,
- generateTypeFromTag,
- generateParamDeclaration,
- generateFunctionDeclaration,
- generateClassDeclaration,
- generateMethodDeclarations,
- generateTypeDefinitions
-} from '../../../utils/helper.mjs';
-
-// Move absFuncDoc to the top level
-const absFuncDoc = {
- 'description': {
- 'type': 'root',
- 'children': [
- {
- 'type': 'paragraph',
- 'children': [
- {
- 'type': 'text',
- 'value': 'Calculates the absolute value of a number.'
- }
- ]
- },
- {
- 'type': 'paragraph',
- 'children': [
- {
- 'type': 'text',
- 'value': "A number's absolute value is its distance from zero on the number line.\n-5 and 5 are both five units away from zero, so calling "
- },
- {
- 'type': 'inlineCode',
- 'value': 'abs(-5)'
- },
- {
- 'type': 'text',
- 'value': ' and\n'
- },
- {
- 'type': 'inlineCode',
- 'value': 'abs(5)'
- },
- {
- 'type': 'text',
- 'value': ' both return 5. The absolute value of a number is always positive.'
- }
- ]
- }
- ]
- },
- 'tags': [
- {
- 'title': 'method',
- 'description': null,
- 'lineNumber': 7,
- 'name': 'abs'
- },
- {
- 'title': 'param',
- 'description': 'number to compute.',
- 'lineNumber': 8,
- 'type': {
- 'type': 'NameExpression',
- 'name': 'Number'
- },
- 'name': 'n'
- },
- {
- 'title': 'return',
- 'description': 'absolute value of given number.',
- 'lineNumber': 9,
- 'type': {
- 'type': 'NameExpression',
- 'name': 'Number'
- }
- },
- {
- 'title': 'example',
- 'description': "\n\nfunction setup() {\n createCanvas(100, 100);\n\n describe('A gray square with a vertical black line that divides it in half. A white rectangle gets taller when the user moves the mouse away from the line.');\n}\n\nfunction draw() {\n background(200);\n\n // Divide the canvas.\n line(50, 0, 50, 100);\n\n // Calculate the mouse's distance from the middle.\n let h = abs(mouseX - 50);\n\n // Draw a rectangle based on the mouse's distance\n // from the middle.\n rect(0, 100 - h, 100, h);\n}\n
\n",
- 'lineNumber': 11
- }
- ],
- 'loc': {
- 'start': {
- 'line': 9,
- 'column': 2,
- 'index': 112
- },
- 'end': {
- 'line': 44,
- 'column': 5,
- 'index': 1167
- }
- },
- 'context': {
- 'loc': {
- 'start': {
- 'line': 45,
- 'column': 2,
- 'index': 1170
- },
- 'end': {
- 'line': 45,
- 'column': 20,
- 'index': 1188
- }
- },
- 'file': 'C:\\Users\\diyas\\Documents\\p5.js\\src\\math\\calculation.js'
- },
- 'augments': [],
- 'examples': [
- {
- 'description': "\n\nfunction setup() {\n createCanvas(100, 100);\n\n describe('A gray square with a vertical black line that divides it in half. A white rectangle gets taller when the user moves the mouse away from the line.');\n}\n\nfunction draw() {\n background(200);\n\n // Divide the canvas.\n line(50, 0, 50, 100);\n\n // Calculate the mouse's distance from the middle.\n let h = abs(mouseX - 50);\n\n // Draw a rectangle based on the mouse's distance\n // from the middle.\n rect(0, 100 - h, 100, h);\n}\n
\n"
- }
- ],
- 'implements': [],
- 'params': [
- {
- 'title': 'param',
- 'name': 'n',
- 'lineNumber': 8,
- 'description': {
- 'type': 'root',
- 'children': [
- {
- 'type': 'paragraph',
- 'children': [
- {
- 'type': 'text',
- 'value': 'number to compute.'
- }
- ]
- }
- ]
- },
- 'type': {
- 'type': 'NameExpression',
- 'name': 'Number'
- }
- }
- ],
- 'properties': [],
- 'returns': [
- {
- 'description': {
- 'type': 'root',
- 'children': [
- {
- 'type': 'paragraph',
- 'children': [
- {
- 'type': 'text',
- 'value': 'absolute value of given number.'
- }
- ]
- }
- ]
- },
- 'title': 'returns',
- 'type': {
- 'type': 'NameExpression',
- 'name': 'Number'
- }
- }
- ],
- 'sees': [],
- 'throws': [],
- 'todos': [],
- 'yields': [],
- 'kind': 'function',
- 'name': 'abs',
- 'members': {
- 'global': [],
- 'inner': [],
- 'instance': [],
- 'events': [],
- 'static': []
- },
- 'path': [
- {
- 'name': 'abs',
- 'kind': 'function'
- }
- ],
- 'namespace': 'abs'
-};
-
-suite('normalizeClassName', () => {
- test('should handle different class name formats', () => {
- expect(normalizeClassName('p5')).toBe('p5');
- expect(normalizeClassName('Vector')).toBe('p5.Vector');
- expect(normalizeClassName('p5.Color')).toBe('p5.Color');
- expect(normalizeClassName()).toBe('p5');
- });
-});
-
-suite('generateTypeFromTag', () => {
- test('should handle primitive types', () => {
- expect(generateTypeFromTag({
- type: { type: 'NameExpression', name: 'Number' }
- })).toBe('number');
- expect(generateTypeFromTag({
- type: { type: 'NameExpression', name: 'String' }
- })).toBe('string');
- expect(generateTypeFromTag({
- type: { type: 'NameExpression', name: 'Boolean' }
- })).toBe('boolean');
- });
-
- test('should handle array types', () => {
- expect(generateTypeFromTag({
- type: {
- type: 'TypeApplication',
- expression: { type: 'NameExpression', name: 'Array' },
- applications: [{ type: 'NameExpression', name: 'Number' }]
- }
- })).toBe('number[]');
- });
-
- test('should handle union types', () => {
- expect(generateTypeFromTag({
- type: {
- type: 'UnionType',
- elements: [
- { type: 'NameExpression', name: 'Number' },
- { type: 'NameExpression', name: 'String' }
- ]
- }
- })).toBe('number | string');
- });
-});
-
-suite('generateParamDeclaration', () => {
- test('should handle required parameters', () => {
- expect(generateParamDeclaration({
- name: 'x',
- type: { type: 'NameExpression', name: 'Number' }
- })).toBe('x: number');
- });
-
- test('should handle optional parameters', () => {
- expect(generateParamDeclaration({
- name: 'y',
- type: {
- type: 'OptionalType',
- expression: { type: 'NameExpression', name: 'String' }
- }
- })).toBe('y?: string');
- });
-
- test('should handle parameters with no type', () => {
- expect(generateParamDeclaration({
- name: 'unknown'
- })).toBe('unknown: any');
- });
-});
-
-suite('generateFunctionDeclaration', () => {
- test('should handle abs() function data', () => {
- const declaration = generateFunctionDeclaration(absFuncDoc);
- console.log(declaration);
- expect(declaration).toContain('function abs(n: number): number;\n\n');
- });
-});
-
-suite('generateClassDeclaration', () => {
- test('should generate correct class declaration for p5.Shader', () => {
- const classDoc = {
- name: 'p5.Shader',
- description: '',
- params: [
- { name: 'renderer', type: { type: 'NameExpression', name: 'p5.RendererGL' } },
- { name: 'vertSrc', type: { type: 'NameExpression', name: 'string' } },
- { name: 'fragSrc', type: { type: 'NameExpression', name: 'string' } },
- {
- name: 'options',
- type: {
- type: 'OptionalType',
- expression: { type: 'NameExpression', name: 'object' }
- }
- }
- ],
- module: 'p5',
- submodule: null,
- extends: null
- };
-
- const organizedData = {
- classitems: [] // Empty since we're just testing class declaration
- };
-
- const declaration = generateClassDeclaration(classDoc, organizedData);
- expect(declaration).toContain('class Shader {\n');
- expect(declaration).toContain('constructor(renderer: p5.RendererGL, vertSrc: string, fragSrc: string, options?: object);\n');
- });
-});
-
-suite('generateMethodDeclarations', () => {
- test('should generate correct method declaration for copyToContext', () => {
- const item = {
- name: 'copyToContext',
- kind: 'function',
- description: '',
- params: [{
- name: 'context',
- type: {
- type: 'UnionType',
- elements: [
- { type: 'NameExpression', name: 'p5' },
- { type: 'NameExpression', name: 'p5.Graphics', optional: false }
- ]
- }
- }],
- returnType: 'p5.Shader',
- class: 'p5.Shader',
- module: 'p5',
- submodule: null,
- isStatic: false,
- overloads: undefined
- };
-
- const declaration = generateMethodDeclarations(item);
- expect(declaration).toContain('copyToContext(context: p5 | p5.Graphics): p5.Shader;\n\n');
- });
-});
-
-suite('generateTypeDefinitions', () => {
- test('should generate type definitions from minimal data', () => {
- const result = generateTypeDefinitions([absFuncDoc]);
-
- const expectedContent = '// This file is auto-generated from JSDoc documentation\n\n' +
- 'import p5 from \'p5\';\n\n' +
- 'declare module \'p5\' {\n' +
- '/**\n' +
- ' * Calculates the absolute value of a number.A number\'s absolute value is its distance from zero on the number line.\n' +
- ' * -5 and 5 are both five units away from zero, so calling `abs(-5)` and\n' +
- ' * `abs(5)` both return 5. The absolute value of a number is always positive.\n' +
- ' *\n' +
- ' * @param number to compute.\n' +
- ' * @return absolute value of given number.\n' +
- ' * @example \n' +
- ' * \n' +
- ' * function setup() {\n' +
- ' * createCanvas(100, 100);\n' +
- ' *\n' +
- ' * describe(\'A gray square with a vertical black line that divides it in half. A white rectangle gets taller when the user moves the mouse away from the line.\');\n' +
- ' * }\n' +
- ' *\n' +
- ' * function draw() {\n' +
- ' * background(200);\n' +
- ' *\n' +
- ' * // Divide the canvas.\n' +
- ' * line(50, 0, 50, 100);\n' +
- ' *\n' +
- ' * // Calculate the mouse\'s distance from the middle.\n' +
- ' * let h = abs(mouseX - 50);\n' +
- ' *\n' +
- ' * // Draw a rectangle based on the mouse\'s distance\n' +
- ' * // from the middle.\n' +
- ' * rect(0, 100 - h, 100, h);\n' +
- ' * }\n' +
- ' *
\n' +
- ' * \n' +
- ' */\n' +
- 'function abs(n: number): number;\n\n' +
- '}\n\n';
-
- const filePath = 'C:\\Users\\diyas\\Documents\\p5.js\\src\\math\\calculation.js';
-
- // Helper function to normalize whitespace and newlines
- const normalizeString = str =>
- str.replace(/\s+/g, ' ')
- .replace(/\n\s*/g, '\n')
- .trim();
-
- // Compare normalized strings
- expect(
- normalizeString(result.fileTypes.get(filePath))
- ).toEqual(
- normalizeString(expectedContent)
- );
-
- // Check global type definitions
- expect(result.globalTypes).toEqual(
- '// This file is auto-generated from JSDoc documentation\n\n' +
- 'import p5 from \'p5\';\n\n' +
- 'declare global {\n' +
- ' /**\n' +
- ' * Calculates the absolute value of a number.A number\'s absolute value is its distance from zero on the number line.\n' +
- ' * -5 and 5 are both five units away from zero, so calling `abs(-5)` and\n' +
- ' * `abs(5)` both return 5. The absolute value of a number is always positive.\n' +
- ' */\n' +
- ' function abs(n: number): number;\n\n' +
- ' interface Window {\n' +
- ' abs: typeof abs;\n' +
- ' }\n' +
- '}\n\n' +
- 'export {};\n'
- );
-
- // Check p5 type definitions
- expect(result.p5Types).toEqual(
- '// This file is auto-generated from JSDoc documentation\n\n' +
- 'declare class p5 {\n' +
- ' constructor(sketch?: (p: p5) => void, node?: HTMLElement, sync?: boolean);\n\n' +
- ' /**\n' +
- ' * Calculates the absolute value of a number.A number\'s absolute value is its distance from zero on the number line.\n' +
- ' * -5 and 5 are both five units away from zero, so calling `abs(-5)` and\n' +
- ' * `abs(5)` both return 5. The absolute value of a number is always positive.\n' +
- ' *\n' +
- ' * @param\n' +
- ' */\n' +
- ' abs(n: number): number;\n\n' +
- '}\n\n' +
- 'declare namespace p5 {\n' +
- '}\n\n' +
- 'export default p5;\n' +
- 'export as namespace p5;\n'
- );
- });
-});
diff --git a/utils/convert.mjs b/utils/convert.mjs
index ba56c59df0..00987eea32 100644
--- a/utils/convert.mjs
+++ b/utils/convert.mjs
@@ -1,106 +1,37 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
-import { getAllEntries } from './helper.mjs';
+import { processData } from './data-processor.mjs';
+import { descriptionString, typeObject } from './shared-helpers.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const data = JSON.parse(fs.readFileSync(path.join(__dirname, '../docs/data.json')));
-const allData = getAllEntries(data);
+// Strategy for HTML documentation output (maintains exact convert.mjs behavior)
+const htmlStrategy = {
+ shouldSkipEntry: () => false, // Don't skip anything (including Foundation)
+
+ processDescription: (desc) => descriptionString(desc),
+
+ processType: (type) => typeObject(type)
+};
+
+const processed = processData(data, htmlStrategy);
const converted = {
project: {}, // Unimplemented, probably not needed
files: {}, // Unimplemented, probably not needed
- modules: {},
- classes: {},
- classitems: [],
+ modules: processed.modules,
+ classes: processed.classes,
+ classitems: processed.classitems,
warnings: [], // Intentionally unimplemented
- consts: {}
+ consts: processed.consts
};
-function descriptionString(node, parent) {
- if (!node) {
- return '';
- } else if (node.type === 'text') {
- return node.value;
- } else if (node.type === 'paragraph') {
- const content = node.children.map(n => descriptionString(n, node)).join('');
- if (parent && parent.children.length === 1) return content;
- return '' + content + '
\n';
- } else if (node.type === 'code') {
- let classes = [];
- let attrs = '';
- if (node.lang) {
- classes.push(`language-${node.lang}`);
- }
- if (node.meta) {
- classes.push(node.meta);
- }
- if (classes.length > 0) {
- attrs=` class="${classes.join(' ')}"`;
- }
- return `${node.value}
`;
- } else if (node.type === 'inlineCode') {
- return '' + node.value + '
';
- } else if (node.type === 'list') {
- const tag = node.type === 'ordered' ? 'ol' : 'ul';
- return `<${tag}>` + node.children.map(n => descriptionString(n, node)).join('') + `${tag}>`;
- } else if (node.type === 'listItem') {
- return '' + node.children.map(n => descriptionString(n, node)).join('') + ' ';
- } else if (node.value) {
- return node.value;
- } else if (node.children) {
- return node.children.map(n => descriptionString(n, node)).join('');
- } else {
- return '';
- }
-}
-
-function typeObject(node) {
- if (!node) return {};
-
- if (node.type === 'OptionalType') {
- return { optional: 1, ...typeObject(node.expression) };
- } else if (node.type === 'UnionType') {
- const names = node.elements.map(n => typeObject(n).type);
- return {
- type: names.join('|')
- };
- } else if (node.type === 'TypeApplication') {
- const { type: typeName } = typeObject(node.expression);
- if (
- typeName === 'Array' &&
- node.applications.length === 1
- ) {
- return {
- type: `${typeObject(node.applications[0]).type}[]`
- };
- }
- const args = node.applications.map(n => typeObject(n).type);
- return {
- type: `${typeName}<${args.join(', ')}>`
- };
- } else if (node.type === 'UndefinedLiteral') {
- return { type: 'undefined' };
- } else if (node.type === 'FunctionType') {
- let signature = `function(${node.params.map(p => typeObject(p).type).join(', ')})`;
- if (node.result) {
- signature += `: ${typeObject(node.result).type}`;
- }
- return { type: signature };
- } else if (node.type === 'ArrayType') {
- return { type: `[${node.elements.map(e => typeObject(e).type).join(', ')}]` };
- } else if (node.type === 'RestType') {
- return { type: typeObject(node.expression).type, rest: true };
- } else {
- // TODO
- // - handle record types
- return { type: node.name };
- }
-}
+// Register constant usage for the original convert.mjs functionality
const constUsage = {};
function registerConstantUsage(name, memberof, node) {
if (!node) return;
@@ -123,375 +54,20 @@ function registerConstantUsage(name, memberof, node) {
}
}
-function locationInfo(node) {
- return {
- file: node.context.file.slice(node.context.file.indexOf('src/')),
- line: node.context.loc.start.line
- };
-}
-
-function deprecationInfo(node) {
- if (!node.deprecated) {
- return {};
- }
-
- return {
- deprecated: true,
- deprecationMessage: descriptionString(node.deprecated)
- };
-}
-
-function getExample(node) {
- return node.description;
-}
-
-function getAlt(node) {
- return node
- .tags
- .filter(tag => tag.title === 'alt')
- .map(tag => tag.description)
- .join('\n') || undefined;
-}
-
-// ============================================================================
-// Modules
-// ============================================================================
-const fileModuleInfo = {};
-const modules = {};
-const submodules = {};
-for (const entry of allData) {
- if (entry.tags.some(tag => tag.title === 'module')) {
- const module = entry.tags.find(tag => tag.title === 'module').name;
-
- const submoduleTag = entry.tags.find(tag => tag.title === 'submodule');
- const submodule = submoduleTag ? submoduleTag.description : undefined;
-
- // TODO handle methods in classes that don't have this
- const forTag = entry.tags.find(tag => tag.title === 'for');
- const forEntry = forTag ? forTag.description : undefined;
-
- const file = entry.context.file;
-
- // Record what module/submodule each file is attached to so that we can
- // look this info up for each method based on its file
- fileModuleInfo[file] = fileModuleInfo[file] || {
- module: undefined,
- submodule: undefined,
- for: undefined
- };
- fileModuleInfo[file].module = module;
- fileModuleInfo[file].submodule =
- fileModuleInfo[file].submodule || submodule;
- fileModuleInfo[file].for =
- fileModuleInfo[file].for || forEntry;
-
- modules[module] = modules[module] || {
- name: module,
- submodules: {},
- classes: {}
- };
- if (submodule) {
- modules[module].submodules[submodule] = 1;
- submodules[submodule] = submodules[submodule] || {
- name: submodule,
- module,
- is_submodule: 1
- };
- }
- }
-}
-for (const key in modules) {
- converted.modules[key] = modules[key];
-}
-for (const key in submodules) {
- // Some modules also list themselves as submodules as a default category
- // of sorts. Skip adding these submodules to not overwrite the module itself.
- if (converted.modules[key]) continue;
- converted.modules[key] = submodules[key];
-}
-
-function getModuleInfo(entry) {
- const entryForTag = entry.tags.find(tag => tag.title === 'for');
- const entryForTagValue = entryForTag && entryForTag.description;
- const file = entry.context.file;
- let { module, submodule, for: forEntry } = fileModuleInfo[file] || {};
- let memberof = entry.memberof;
- if (memberof === 'fn') memberof = 'p5';
- if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) {
- memberof = 'p5.' + memberof;
+// Register constant usage from processed data
+for (const item of converted.classitems) {
+ if (item.itemtype === 'property' && (item.name in converted.consts || item.kind === 'constant' || item.kind === 'typedef')) {
+ constUsage[item.name] = constUsage[item.name] || new Set();
}
- forEntry = memberof || entryForTagValue || forEntry;
- return { module, submodule, forEntry };
-}
-
-function getParams(entry) {
- // Documentation.js seems to try to grab params from the function itself in
- // the code if we don't document all the parameters. This messes with our
- // manually-documented overloads. Instead of using the provided entry.params
- // array, we'll instead only rely on manually included @param tags.
- //
- // However, the tags don't include a tree-structured description field, and
- // instead convert it to a string. We want a slightly different conversion to
- // string, so we match these params to the Documentation.js-provided `params`
- // array and grab the description from those.
- return (entry.tags || [])
-
- // Filter out the nested parameters (eg. options.extrude),
- // to be treated as part of parent parameters (eg. options)
- // and not separate entries
- .filter(t => t.title === 'param' && !t.name.includes('.'))
- .map(node => {
- const param = (entry.params || [])
- .find(param => param.name === node.name);
- return {
- ...node,
- description: param?.description || {
- type: 'html',
- value: node.description
- }
- };
- });
-}
-
-// ============================================================================
-// Constants
-// ============================================================================
-for (const entry of allData) {
- if (entry.kind === 'constant' || entry.kind === 'typedef') {
- constUsage[entry.name] = constUsage[entry.name] || new Set();
-
- const { module, submodule, forEntry } = getModuleInfo(entry);
-
- const examples = entry.examples.map(getExample);
- const item = {
- itemtype: 'property',
- name: entry.name,
- ...locationInfo(entry),
- ...typeObject(entry.type),
- ...deprecationInfo(entry),
- description: descriptionString(entry.description),
- example: examples.length > 0 ? examples : undefined,
- alt: getAlt(entry),
- module,
- submodule,
- class: forEntry || 'p5'
- };
-
- converted.classitems.push(item);
- }
-}
-
-// ============================================================================
-// Classes
-// ============================================================================
-for (const entry of allData) {
- if (entry.kind === 'class') {
- const { module, submodule } = getModuleInfo(entry);
-
- const item = {
- name: entry.name,
- ...locationInfo(entry),
- ...deprecationInfo(entry),
- extends: entry.augments && entry.augments[0] && entry.augments[0].name,
- description: descriptionString(entry.description),
- example: entry.examples.map(getExample),
- alt: getAlt(entry),
- params: getParams(entry).map(p => {
- return {
- name: p.name,
- description: p.description && descriptionString(p.description),
- ...typeObject(p.type)
- };
- }),
- return: entry.returns[0] && {
- description: descriptionString(entry.returns[0].description),
- ...typeObject(entry.returns[0].type)
- },
- is_constructor: 1,
- module,
- submodule
- };
-
- // The @private tag doesn't seem to end up in the Documentation.js output.
- // However, it also doesn't seem to grab the description in this case, so
- // I'm using this as a proxy to let us know that a class should be private.
- // This means any public class *must* have a description.
- const isPrivate = !item.description;
- if (!isPrivate) {
- converted.classes[item.name] = item;
- }
- }
-}
-
-// ============================================================================
-// Class properties
-// ============================================================================
-const propDefs = {};
-
-// Grab properties out of the class nodes. These should have all the properties
-// but very little of their metadata.
-for (const entry of allData) {
- if (entry.kind !== 'class') continue;
-
- // Ignore private classes
- if (!converted.classes[entry.name]) continue;
-
- if (!entry.properties) continue;
-
- const { module, submodule } = getModuleInfo(entry);
- const location = locationInfo(entry);
- propDefs[entry.name] = propDefs[entry.name] || {};
-
- for (const property of entry.properties) {
- const item = {
- itemtype: 'property',
- name: property.name,
- ...location,
- line: property.lineNumber || location.line,
- ...typeObject(property.type),
- ...deprecationInfo(entry),
- module,
- submodule,
- class: entry.name
- };
- propDefs[entry.name][property.name] = item;
- }
-}
-
-// Grab property metadata out of other loose nodes.
-for (const entry of allData) {
- // These are in a different section
- if (entry.kind === 'constant') continue;
-
- const { module, submodule, forEntry } = getModuleInfo(entry);
- const propTag = entry.tags.find(tag => tag.title === 'property');
- const forTag = entry.tags.find(tag => tag.title === 'for');
- let memberof = entry.memberof;
- if (memberof === 'fn') memberof = 'p5';
- if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) {
- memberof = 'p5.' + memberof;
- }
- if (!propTag || (!forEntry && !forTag && !memberof)) continue;
-
- const forName = memberof || (forTag && forTag.description) || forEntry;
- propDefs[forName] = propDefs[forName] || {};
- const classEntry = propDefs[forName];
- if (!classEntry) continue;
-
- registerConstantUsage(entry.type);
-
- const prop = classEntry[propTag.name] || {
- itemtype: 'property',
- name: propTag.name,
- ...locationInfo(entry),
- ...typeObject(propTag.type),
- ...deprecationInfo(entry),
- module,
- submodule,
- class: forName
- };
-
- const updated = {
- ...prop,
- example: entry.examples.map(getExample),
- alt: getAlt(entry),
- description: descriptionString(entry.description)
- };
- classEntry[propTag.name] = updated;
-}
-
-// Add to the list
-for (const className in propDefs) {
- for (const propName in propDefs[className]) {
- converted.classitems.push(propDefs[className][propName]);
- }
-}
-
-// ============================================================================
-// Class methods
-// ============================================================================
-const classMethods = {};
-for (const entry of allData) {
- if (entry.kind === 'function' && entry.properties.length === 0) {
- const { module, submodule, forEntry } = getModuleInfo(entry);
-
- let memberof = entry.memberof;
- if (memberof === 'fn') memberof = 'p5';
- if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) {
- memberof = 'p5.' + memberof;
- }
-
- // Ignore functions that aren't methods
- if (entry.tags.some(tag => tag.title === 'function')) continue;
-
- // If a previous version of this same method exists, then this is probably
- // an overload on that method
- const prevItem = (classMethods[memberof] || {})[entry.name] || {};
-
- const className = memberof || prevItem.class || forEntry;
-
- // Ignore methods of private classes
- if (!converted.classes[className]) continue;
-
- // Ignore private methods. @private-tagged ones don't show up in the JSON,
- // but we also implicitly use this _-prefix convension.
- const isPrivate = entry.name.startsWith('_');
- if (isPrivate) continue;
-
- for (const param of getParams(entry)) {
- registerConstantUsage(entry.name, className, param.type);
- }
- if (entry.returns[0]) {
- registerConstantUsage(entry.returns[0].type);
+ if (item.itemtype === 'method') {
+ for (const overload of item.overloads || []) {
+ for (const param of overload.params || []) {
+ registerConstantUsage(item.name, item.class, param.type);
+ }
+ if (overload.return) {
+ registerConstantUsage(item.name, item.class, overload.return.type);
+ }
}
-
- const item = {
- name: entry.name,
- ...locationInfo(entry),
- ...deprecationInfo(entry),
- itemtype: 'method',
- chainable: (prevItem.chainable || entry.tags.some(tag => tag.title === 'chainable'))
- ? 1
- : undefined,
- description: prevItem.description || descriptionString(entry.description),
- example: [
- ...(prevItem.example || []),
- ...entry.examples.map(getExample)
- ],
- alt: getAlt(entry),
- overloads: [
- ...(prevItem.overloads || []),
- {
- params: getParams(entry).map(p => {
- return {
- name: p.name,
- description: p.description && descriptionString(p.description),
- ...typeObject(p.type)
- };
- }),
- return: entry.returns[0] && {
- description: descriptionString(entry.returns[0].description),
- ...typeObject(entry.returns[0].type)
- }
- }
- ],
- return: prevItem.return || entry.returns[0] && {
- description: descriptionString(entry.returns[0].description),
- ...typeObject(entry.returns[0].type)
- },
- class: className,
- static: entry.scope === 'static' && 1,
- module,
- submodule
- };
-
- classMethods[memberof] = classMethods[memberof] || {};
- classMethods[memberof][entry.name] = item;
- }
-}
-for (const className in classMethods) {
- for (const methodName in classMethods[className]) {
- converted.classitems.push(classMethods[className][methodName]);
}
}
diff --git a/utils/data-processor.mjs b/utils/data-processor.mjs
new file mode 100644
index 0000000000..99f3c44d91
--- /dev/null
+++ b/utils/data-processor.mjs
@@ -0,0 +1,283 @@
+import { getAllEntries } from './shared-helpers.mjs';
+import { getParams } from './shared-helpers.mjs';
+
+/**
+ * Common data processing logic that can be used by both convert.mjs and typescript.mjs
+ * with different strategies for type conversion and output formatting.
+ */
+
+export function processData(rawData, strategy) {
+ const allData = getAllEntries(rawData);
+
+ const processed = {
+ modules: {},
+ classes: {},
+ classitems: [],
+ consts: {},
+ classMethods: {}
+ };
+
+ // Build module info lookup (exact same logic as convert.mjs)
+ const fileModuleInfo = {};
+ const modules = {};
+ const submodules = {};
+
+ for (const entry of allData) {
+ if (entry.tags?.some(tag => tag.title === 'module')) {
+ const module = entry.tags.find(tag => tag.title === 'module').name;
+ const submoduleTag = entry.tags.find(tag => tag.title === 'submodule');
+ const submodule = submoduleTag ? submoduleTag.description : undefined;
+ const forTag = entry.tags.find(tag => tag.title === 'for');
+ const forEntry = forTag ? forTag.description : undefined;
+ const file = entry.context.file;
+
+ fileModuleInfo[file] = fileModuleInfo[file] || {
+ module: undefined,
+ submodule: undefined,
+ for: undefined
+ };
+ fileModuleInfo[file].module = module;
+ fileModuleInfo[file].submodule = fileModuleInfo[file].submodule || submodule;
+ fileModuleInfo[file].for = fileModuleInfo[file].for || forEntry;
+
+ modules[module] = modules[module] || {
+ name: module,
+ submodules: {},
+ classes: {}
+ };
+ if (submodule) {
+ modules[module].submodules[submodule] = 1;
+ submodules[submodule] = submodules[submodule] || {
+ name: submodule,
+ module,
+ is_submodule: 1
+ };
+ }
+ }
+ }
+
+ // Copy modules to processed data
+ for (const key in modules) {
+ processed.modules[key] = modules[key];
+ }
+ for (const key in submodules) {
+ if (processed.modules[key]) continue;
+ processed.modules[key] = submodules[key];
+ }
+
+ function getModuleInfo(entry) {
+ const entryForTag = entry.tags?.find(tag => tag.title === 'for');
+ const entryForTagValue = entryForTag?.description;
+ const file = entry.context?.file;
+ let { module, submodule, for: forEntry } = fileModuleInfo[file] || {};
+ let memberof = entry.memberof;
+ if (memberof === 'fn') memberof = 'p5';
+ if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) {
+ memberof = 'p5.' + memberof;
+ }
+ forEntry = memberof || entryForTagValue || forEntry;
+ return { module, submodule, forEntry };
+ }
+
+ function locationInfo(entry) {
+ return {
+ file: entry.context?.file ? entry.context.file.slice(entry.context.file.indexOf('src/')) : '',
+ line: entry.context?.loc?.start?.line || 1
+ };
+ }
+
+ function deprecationInfo(entry) {
+ if (!entry.deprecated) {
+ return {};
+ }
+ return {
+ deprecated: true,
+ deprecationMessage: strategy.processDescription(entry.deprecated)
+ };
+ }
+
+ function getExample(entry) {
+ return entry.description;
+ }
+
+ function getAlt(entry) {
+ return entry
+ .tags
+ ?.filter(tag => tag.title === 'alt')
+ ?.map(tag => tag.description)
+ ?.join('\n') || undefined;
+ }
+
+ // Process constants, typedefs, and properties
+ const processedNames = new Set();
+ for (const entry of allData) {
+ if (entry.kind === 'constant' || entry.kind === 'typedef' || entry.kind === 'property' ||
+ (entry.properties && entry.properties.length > 0 && entry.properties[0].title === 'property') ||
+ entry.tags?.some(tag => tag.title === 'property')) {
+ const { module, submodule, forEntry } = getModuleInfo(entry);
+
+ // Apply strategy filter
+ if (strategy.shouldSkipEntry && strategy.shouldSkipEntry(entry, { module, submodule, forEntry })) {
+ continue;
+ }
+
+ const name = entry.name || (entry.properties || [])[0]?.name;
+
+ // Skip duplicates based on name + class combination
+ const key = `${name}:${forEntry || 'p5'}`;
+ if (processedNames.has(key)) {
+ continue;
+ }
+ processedNames.add(key);
+
+
+ // For properties, get type from the property definition
+ const propertyType = entry.properties?.[0]?.type || entry.type;
+
+ const examples = entry.examples?.map(getExample) || [];
+ const item = {
+ itemtype: 'property',
+ name,
+ ...locationInfo(entry),
+ ...strategy.processType(propertyType, entry),
+ ...deprecationInfo(entry),
+ description: strategy.processDescription(entry.description),
+ example: examples.length > 0 ? examples : undefined,
+ alt: getAlt(entry),
+ module,
+ submodule,
+ class: forEntry || 'p5'
+ };
+
+ processed.classitems.push(item);
+ processed.consts[name] = item;
+ }
+ }
+
+ // Process classes
+ for (const entry of allData) {
+ if (entry.kind === 'class') {
+ const { module, submodule } = getModuleInfo(entry);
+
+ // Apply strategy filter
+ if (strategy.shouldSkipEntry && strategy.shouldSkipEntry(entry, { module, submodule })) {
+ continue;
+ }
+
+ const item = {
+ name: entry.name,
+ ...locationInfo(entry),
+ ...deprecationInfo(entry),
+ extends: entry.augments?.[0]?.name,
+ description: strategy.processDescription(entry.description),
+ example: entry.examples?.map(getExample) || [],
+ alt: getAlt(entry),
+ params: getParams(entry).map(p => ({
+ name: p.name,
+ description: p.description && strategy.processDescription(p.description),
+ ...strategy.processType(p.type, p)
+ })),
+ return: entry.returns?.[0] && {
+ description: strategy.processDescription(entry.returns[0].description),
+ ...strategy.processType(entry.returns[0].type)
+ },
+ is_constructor: 1,
+ module,
+ submodule
+ };
+
+ // The @private tag doesn't seem to end up in the Documentation.js output.
+ // However, it also doesn't seem to grab the description in this case, so
+ // I'm using this as a proxy to let us know that a class should be private.
+ // This means any public class *must* have a description.
+ const isPrivate = !item.description;
+ if (!isPrivate) {
+ processed.classes[item.name] = item;
+ }
+ }
+ }
+
+ // Process methods and functions
+ for (const entry of allData) {
+ if (entry.kind === 'function' && entry.properties?.length === 0) {
+ const { module, submodule, forEntry } = getModuleInfo(entry);
+
+ // Apply strategy filter
+ if (strategy.shouldSkipEntry && strategy.shouldSkipEntry(entry, { module, submodule, forEntry })) {
+ continue;
+ }
+
+ // Skip functions that aren't methods
+ if (entry.tags?.some(tag => tag.title === 'function')) continue;
+
+ let memberof = entry.memberof;
+ if (memberof === 'fn') memberof = 'p5';
+ if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) {
+ memberof = 'p5.' + memberof;
+ }
+
+ const className = memberof || forEntry || 'p5';
+
+ // Skip private methods
+ if (entry.name.startsWith('_')) continue;
+
+ // Skip methods of private classes
+ if (!processed.classes[className] && className !== 'p5') continue;
+
+ // Check for existing method (overloads) - distinguish static vs instance
+ const isStatic = entry.scope === 'static';
+ const methodKey = isStatic ? `static_${entry.name}` : entry.name;
+ const prevItem = processed.classMethods[className]?.[methodKey];
+
+ const item = {
+ name: entry.name,
+ ...locationInfo(entry),
+ ...deprecationInfo(entry),
+ itemtype: 'method',
+ chainable: (prevItem?.chainable || entry.tags?.some(tag => tag.title === 'chainable'))
+ ? 1
+ : undefined,
+ description: prevItem?.description || strategy.processDescription(entry.description),
+ example: [
+ ...(prevItem?.example || []),
+ ...entry.examples?.map(getExample) || []
+ ],
+ alt: getAlt(entry),
+ overloads: [
+ ...(prevItem?.overloads || []),
+ {
+ params: getParams(entry).map(p => ({
+ name: p.name,
+ description: p.description && strategy.processDescription(p.description),
+ ...strategy.processType(p.type, p)
+ })),
+ return: entry.returns?.[0] && {
+ description: strategy.processDescription(entry.returns[0].description),
+ ...strategy.processType(entry.returns[0].type)
+ }
+ }
+ ],
+ return: prevItem?.return || entry.returns?.[0] && {
+ description: strategy.processDescription(entry.returns[0].description),
+ ...strategy.processType(entry.returns[0].type)
+ },
+ class: className,
+ static: entry.scope === 'static' && 1,
+ module,
+ submodule
+ };
+
+ processed.classMethods[className] = processed.classMethods[className] || {};
+ processed.classMethods[className][methodKey] = item;
+ }
+ }
+
+ // Add classMethods to classitems for compatibility
+ for (const className in processed.classMethods) {
+ for (const methodName in processed.classMethods[className]) {
+ processed.classitems.push(processed.classMethods[className][methodName]);
+ }
+ }
+
+ return processed;
+}
\ No newline at end of file
diff --git a/utils/generate-types.mjs b/utils/generate-types.mjs
deleted file mode 100644
index 5c5c05a7f1..0000000000
--- a/utils/generate-types.mjs
+++ /dev/null
@@ -1,84 +0,0 @@
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import {
- generateTypeDefinitions
-} from './helper.mjs';
-
-// Fix for __dirname equivalent in ES modules
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-const data = JSON.parse(fs.readFileSync(path.join(__dirname, '../docs/data.json')));
-
-function findDtsFiles(dir, files = []) {
- // Only search in src directory
- const srcDir = path.join(__dirname, '../types');
- if (!dir.startsWith(srcDir)) {
- dir = srcDir;
- }
-
- const entries = fs.readdirSync(dir, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = path.join(dir, entry.name);
- if (entry.isDirectory()) {
- findDtsFiles(fullPath, files);
- } else if (entry.name.endsWith('.d.ts')) {
- // Get path relative to project root and normalize to forward slashes
- const relativePath = path.relative(path.join(__dirname, '../types'), fullPath)
- .split(path.sep)
- .join('/');
- files.push(relativePath);
- }
- }
- return files;
-}
-
-export function generateAllDeclarationFiles() {
- const {
- p5Types: rawP5Types,
- globalTypes,
- fileTypes
- } = generateTypeDefinitions(data);
- const typesDir = path.join(process.cwd(), 'types');
- fs.mkdirSync(typesDir, { recursive: true });
-
- // Write file-specific type definitions
- fileTypes.forEach((content, filePath) => {
- const parsedPath = path.parse(filePath);
- const relativePath = path.relative(
- path.join(__dirname, '../src'),
- filePath
- );
- const dtsPath = path.join(
- path.relative(process.cwd(), typesDir),
- path.dirname(relativePath),
- `${parsedPath.name}.d.ts`
- );
-
- const exportName = parsedPath.name.replace('.', '_');
- const contentWithExport = content + `export default function ${exportName}(p5: any, fn: any): void;\n`;
-
- fs.mkdirSync(path.dirname(dtsPath), { recursive: true });
- fs.writeFileSync(dtsPath, contentWithExport, 'utf8');
- console.log(`Generated ${dtsPath}`);
- });
-
- // Add .d.ts references to p5Types
- let p5Types = '// This file is auto-generated from JSDoc documentation\n\n';
- p5Types += '/// \n';
-
- // Add references to all other .d.ts files
- const dtsFiles = findDtsFiles(path.join(__dirname, '..'));
- for (const file of dtsFiles) {
- p5Types += `/// \n`;
- }
- p5Types += '\n';
- p5Types += rawP5Types;
-
- fs.writeFileSync(path.join(typesDir, 'p5.d.ts'), p5Types, 'utf8');
- fs.writeFileSync(path.join(typesDir, 'global.d.ts'), globalTypes, 'utf8');
-}
-
-generateAllDeclarationFiles();
diff --git a/utils/helper.mjs b/utils/helper.mjs
deleted file mode 100644
index 5506363692..0000000000
--- a/utils/helper.mjs
+++ /dev/null
@@ -1,626 +0,0 @@
-function getEntries(entry) {
- return [
- entry,
- ...getAllEntries(entry.members?.global || []),
- ...getAllEntries(entry.members?.inner || []),
- ...getAllEntries(entry.members?.instance || []),
- ...getAllEntries(entry.members?.events || []),
- ...getAllEntries(entry.members?.static || [])
- ];
-}
-
-export function getAllEntries(arr = []) {
- return arr.flatMap(entry => entry ? getEntries(entry) : []);
-}
-export function normalizeClassName(className) {
- if (!className || className === 'p5') return 'p5';
- return className.startsWith('p5.') ? className : `p5.${className}`;
-}
-
-export function generateTypeDefinitions(data) {
-
- const organized = organizeData(data);
-
- return {
- p5Types: generateP5TypeDefinitions(organized),
- globalTypes: generateGlobalTypeDefinitions(organized),
- fileTypes: generateFileTypeDefinitions(organized, data)
- };
-}
-function generateP5TypeDefinitions(organizedData) {
- let output = '// This file is auto-generated from JSDoc documentation\n\n';
-
- output += 'declare class p5 {\n';
- output += ' constructor(sketch?: (p: p5) => void, node?: HTMLElement, sync?: boolean);\n\n';
- const instanceItems = organizedData.classitems.filter(item =>
- item.class === 'p5' && !item.isStatic
- );
- instanceItems.forEach(item => {
- output += generateMethodDeclarations(item, false);
- });
-
- const staticItems = organizedData.classitems.filter(item =>
- item.class === 'p5' && item.isStatic
- );
- staticItems.forEach(item => {
- output += generateMethodDeclarations(item, true);
- });
-
- Object.values(organizedData.consts).forEach(constData => {
- if (constData.class === 'p5') {
- if (constData.description) {
- output += ` /**\n * ${constData.description}\n */\n`;
- }
- if (constData.kind === 'constant') {
- output += ` readonly ${constData.name.toUpperCase()}: ${constData.type};\n\n`;
- } else {
- output += ` static ${constData.name}: ${constData.type};\n\n`;
- }
- }
- });
-
- output += '}\n\n';
-
- output += 'declare namespace p5 {\n';
-
- Object.values(organizedData.consts).forEach(constData => {
- if (constData.kind === 'typedef') {
- if (constData.description) {
- output += ` /**\n * ${constData.description}\n */\n`;
- }
- output += ` type ${constData.name} = ${constData.type};\n\n`;
- }
- });
-
- Object.values(organizedData.classes).forEach(classDoc => {
- if (classDoc.name !== 'p5') {
- output += generateClassDeclaration(classDoc, organizedData);
- }
- });
- output += '}\n\n';
-
- output += 'export default p5;\n';
- output += 'export as namespace p5;\n';
-
- return output;
-}
-
-function generateGlobalTypeDefinitions(organizedData) {
- let output = '// This file is auto-generated from JSDoc documentation\n\n';
- output += 'import p5 from \'p5\';\n\n';
- output += 'declare global {\n';
-
- const instanceItems = organizedData.classitems.filter(item =>
- item.class === 'p5' && !item.isStatic
- );
- instanceItems.forEach(item => {
- if (item.kind === 'function') {
- if (item.description) {
- output += ` /**\n${formatJSDocComment(item.description, 2)}\n */\n`;
- }
-
- if (item.overloads?.length > 0) {
- item.overloads.forEach(overload => {
- const params = (overload.params || [])
- .map(param => generateParamDeclaration(param))
- .join(', ');
- const returnType = overload.returns?.[0]?.type
- ? generateTypeFromTag(overload.returns[0])
- : 'void';
- output += ` function ${item.name}(${params}): ${returnType};\n`;
- });
- }
-
- const params = (item.params || [])
- .map(param => generateParamDeclaration(param))
- .join(', ');
- output += ` function ${item.name}(${params}): ${item.returnType};\n\n`;
- }
- });
-
- Object.values(organizedData.consts).forEach(constData => {
- if (constData.kind === 'constant') {
- if (constData.description) {
- output += ` /**\n${formatJSDocComment(constData.description, 2)}\n */\n`;
- }
- output += ` const ${constData.name.toUpperCase()}: p5.${constData.name.toUpperCase()};\n\n`;
- }
- });
-
- output += ' interface Window {\n';
-
- instanceItems.forEach(item => {
- if (item.kind === 'function') {
- output += ` ${item.name}: typeof ${item.name};\n`;
- }
- });
-
- Object.values(organizedData.consts).forEach(constData => {
- if (constData.kind === 'constant') {
- if (constData.description) {
- output += ` /**\n * ${constData.description}\n */\n`;
- }
- output += ` readonly ${constData.name.toUpperCase()}: typeof ${constData.name.toUpperCase()};\n`;
- }
- });
-
- output += ' }\n';
- output += '}\n\n';
- output += 'export {};\n';
-
- return output;
-}
-
-function generateFileTypeDefinitions(organizedData, data) {
- const fileDefinitions = new Map();
- const fileGroups = groupByFile(getAllEntries(data));
-
- fileGroups.forEach((items, filePath) => {
- const declarationContent = generateDeclarationFile(items, organizedData);
- fileDefinitions.set(filePath, declarationContent);
- });
-
- return fileDefinitions;
-}
-const organized = {
- modules: {},
- classes: {},
- classitems: [],
- consts: {}
-};
-
-function generateDeclarationFile(items, organizedData) {
- let output = '// This file is auto-generated from JSDoc documentation\n\n';
- const imports = new Set(['import p5 from \'p5\';']);
- const hasColorDependency = items.some(item => {
- const typeName = item.type?.name;
- const desc = extractDescription(item.description);
- return typeName === 'Color' || (typeof desc === 'string' && desc.includes('Color'));
- });
-
- const hasVectorDependency = items.some(item => {
- const typeName = item.type?.name;
- const desc = extractDescription(item.description);
- return typeName === 'Vector' || (typeof desc === 'string' && desc.includes('Vector'));
- });
-
- const hasConstantsDependency = items.some(item =>
- item.tags?.some(tag => tag.title === 'requires' && tag.description === 'constants')
- );
-
- if (hasColorDependency) {
- imports.add('import { Color } from \'../color/p5.Color\';');
- }
- if (hasVectorDependency) {
- imports.add('import { Vector } from \'../math/p5.Vector\';');
- }
- if (hasConstantsDependency) {
- imports.add('import * as constants from \'../core/constants\';');
- }
-
- output += Array.from(imports).join('\n') + '\n\n';
- output += 'declare module \'p5\' {\n';
-
- const classDoc = items.find(item => item.kind === 'class');
- if (classDoc) {
- const fullClassName = normalizeClassName(classDoc.name);
- const classDocName = fullClassName.replace('p5.', '');
- let parentClass = classDoc.tags?.find(tag => tag.title === 'extends')?.name;
- if (parentClass) {
- parentClass = parentClass.replace('p5.', '');
- }
- const extendsClause = parentClass ? ` extends ${parentClass}` : '';
-
- output += ` class ${classDocName}${extendsClause} {\n`;
-
- if (classDoc.params?.length > 0) {
- output += ' constructor(';
- output += classDoc.params
- .map(param => generateParamDeclaration(param))
- .join(', ');
- output += ');\n\n';
- }
-
- const classItems = organizedData.classitems.filter(item =>
- item.class === fullClassName ||
- item.class === fullClassName.replace('p5.', '')
- );
-
- const staticItems = classItems.filter(item => item.isStatic);
- const instanceItems = classItems.filter(item => !item.isStatic);
- staticItems.forEach(item => {
- output += generateMethodDeclarations(item, true);
- });
- instanceItems.forEach(item => {
- output += generateMethodDeclarations(item, false);
- });
- output += ' }\n\n';
- }
-
- items.forEach(item => {
- if (item.kind !== 'class' && (!item.memberof || item.memberof !== classDoc?.name)) {
- switch (item.kind) {
- case 'function':
- output += generateFunctionDeclaration(item);
- break;
- case 'constant':
- case 'typedef':
- const constData = organizedData.consts[item.name];
- if (constData) {
- if (constData.description) {
- output += ` /**\n * ${constData.description}\n */\n`;
- }
- if (constData.kind === 'constant') {
- output += ` const ${constData.name}: ${constData.type};\n\n`;
- } else {
- output += ` type ${constData.name} = ${constData.type};\n\n`;
- }
- }
- break;
- }
- }
- });
-
- output += '}\n\n';
-
- return output;
-}
-
-export function organizeData(data) {
- const allData = getAllEntries(data);
-
- organized.modules = {};
- organized.classes = {};
- organized.classitems = [];
- organized.consts = {};
-
- allData.forEach(entry => {
- const { module, submodule, forEntry } = getModuleInfo(entry);
- const className = normalizeClassName(forEntry || entry.memberof || 'p5');
-
- switch(entry.kind) {
- case 'class':
- organized.classes[className] = {
- name: entry.name,
- description: extractDescription(entry.description),
- params: (entry.params || []).map(param => ({
- name: param.name,
- type: generateTypeFromTag(param),
- optional: param.type?.type === 'OptionalType'
- })),
- module,
- submodule,
- extends: entry.tags?.find(tag => tag.title === 'extends')?.name || null
- }; break;
- case 'function':
- case 'property':
- const overloads = entry.overloads?.map(overload => ({
- params: overload.params,
- returns: overload.returns,
- description: extractDescription(overload.description)
- }));
-
- organized.classitems.push({
- name: entry.name,
- kind: entry.kind,
- description: extractDescription(entry.description),
- params: (entry.params || []).map(param => ({
- name: param.name,
- type: generateTypeFromTag(param),
- optional: param.type?.type === 'OptionalType'
- })),
- returnType: entry.returns?.[0] ? generateTypeFromTag(entry.returns[0]) : 'void',
- module,
- submodule,
- class: className,
- isStatic: entry.path?.[0]?.scope === 'static',
- overloads
- }); break;
- case 'constant':
- case 'typedef':
- organized.consts[entry.name] = {
- name: entry.name,
- kind: entry.kind,
- description: extractDescription(entry.description),
- type: entry.kind === 'constant' ? `P5.${entry.name.toUpperCase()}` : (entry.type ? generateTypeFromTag(entry) : 'any'),
- module,
- submodule,
- class: forEntry || 'p5'
- }; break;
- }
- });
- return organized;
-}
-
-export function getModuleInfo(entry) {
- return {
- module: entry.tags?.find(tag => tag.title === 'module')?.name || 'p5',
- submodule: entry.tags?.find(tag => tag.title === 'submodule')?.description || null,
- forEntry: entry.tags?.find(tag => tag.title === 'for')?.description || entry.memberof
- };
-}
-export function extractDescription(desc) {
- if (!desc) return '';
- if (typeof desc === 'string') return desc;
- if (desc.children) {
- return desc.children.map(child => {
- if (child.type === 'text') return child.value;
- if (child.type === 'paragraph') return extractDescription(child);
- if (child.type === 'inlineCode' || child.type === 'code') return `\`${child.value}\``;
- return '';
- })
- .join('').trim().replace(/\n{3,}/g, '\n\n');
- }
- return '';
-}
-export function generateTypeFromTag(param) {
- if (!param || !param.type) return 'any';
-
- switch (param.type.type) {
- case 'NameExpression':
- return normalizeTypeName(param.type.name);
- case 'TypeApplication': {
- const baseType = normalizeTypeName(param.type.expression.name);
-
- if (baseType === 'Array') {
- const innerType = param.type.applications[0];
- const innerTypeStr = generateTypeFromTag({ type: innerType });
- return `${innerTypeStr}[]`;
- }
-
- const typeParams = param.type.applications
- .map(app => generateTypeFromTag({ type: app }))
- .join(', ');
- return `${baseType}<${typeParams}>`;
- }
- case 'UnionType':
- const unionTypes = param.type.elements
- .map(el => generateTypeFromTag({ type: el }))
- .join(' | ');
- return unionTypes;
- case 'OptionalType':
- return generateTypeFromTag({ type: param.type.expression });
- case 'AllLiteral':
- return 'any';
- case 'RecordType':
- return 'object';
- case 'StringLiteralType':
- return `'${param.type.value}'`;
- case 'UndefinedLiteralType':
- return 'undefined';
- case 'ArrayType': {
- const innerTypeStrs = param.type.elements
- .map(e => generateTypeFromTag({ type: e }));
- return `[${innerTypeStrs.join(', ')}]`;
- }
- case 'RestType':
- return `${generateTypeFromTag({ type: param.type.expression })}[]`;
- default:
- return 'any';
- }
-}
-
-export function normalizeTypeName(type) {
- if (!type) return 'any';
-
- if (type === '[object Object]') return 'any';
-
- const primitiveTypes = {
- 'String': 'string',
- 'Number': 'number',
- 'Integer': 'number',
- 'Boolean': 'boolean',
- 'Void': 'void',
- 'Object': 'object',
- 'Array': 'Array',
- 'Function': 'Function'
- };
-
- return primitiveTypes[type] || type;
-}
-
-export function generateParamDeclaration(param) {
- if (!param) return 'any';
-
- let type = param.type;
- let prefix = '';
- const isOptional = param.type?.type === 'OptionalType';
- if (typeof type === 'string') {
- type = normalizeTypeName(type);
- } else if (param.type?.type) {
- type = generateTypeFromTag(param);
- } else {
- type = 'any';
- }
-
- if (param.type?.type === 'RestType') {
- prefix = '...';
- }
-
- return `${prefix}${param.name}${isOptional ? '?' : ''}: ${type}`;
-}
-
-export function generateFunctionDeclaration(funcDoc) {
-
- let output = '';
-
- if (funcDoc.description || funcDoc.tags?.length > 0) {
- output += '/**\n';
- const description = extractDescription(funcDoc.description);
- if (description) {
- output += formatJSDocComment(description) + '\n';
- }
- if (funcDoc.tags) {
- if (description) {
- output += ' *\n';
- }
- funcDoc.tags.forEach(tag => {
- if (tag.description) {
- const tagDesc = extractDescription(tag.description);
- output += formatJSDocComment(`@${tag.title} ${tagDesc}`, 0) + '\n';
- }
- });
- }
- output += ' */\n';
- }
-
- const params = (funcDoc.params || [])
- .map(param => generateParamDeclaration(param))
- .join(', ');
-
- const returnType = funcDoc.returns?.[0]?.type
- ? generateTypeFromTag(funcDoc.returns[0])
- : 'void';
-
- output += `function ${funcDoc.name}(${params}): ${returnType};\n\n`;
- return output;
-}
-
-export function generateMethodDeclarations(
- item,
- isStatic = false,
- isGlobal = false
-) {
- let output = '';
-
- if (item.description) {
- output += ' /**\n';
- const itemDesc = extractDescription(item.description);
- output += formatJSDocComment(itemDesc, 2) + '\n';
- if (item.params?.length > 0) {
- output += ' *\n';
- item.params.forEach(param => {
- const paramDesc = extractDescription(param.description);
- output += formatJSDocComment(`@param ${paramDesc}`, 2) + '\n';
- });
- }
- if (item.returns) {
- output += ' *\n';
- const returnDesc = extractDescription(item.returns[0]?.description);
- output += formatJSDocComment(`@return ${returnDesc}`, 2) + '\n';
- }
- output += ' */\n';
- }
-
- if (item.kind === 'function') {
- const staticPrefix = isStatic ? 'static ' : '';
-
- if (item.overloads?.length > 0) {
- item.overloads.forEach(overload => {
- const params = (overload.params || [])
- .map(param => generateParamDeclaration(param))
- .join(', ');
- const returnType = overload.returns?.[0]?.type
- ? generateTypeFromTag(overload.returns[0])
- : 'void';
- output += ` ${staticPrefix}${item.name}(${params}): ${returnType};\n`;
- });
- }
-
- const params = (item.params || [])
- .map(param => generateParamDeclaration(param))
- .join(', ');
- output += ` ${staticPrefix}${item.name}(${params}): ${item.returnType};\n\n`;
- } else {
- const staticPrefix = isStatic ? 'static ' : '';
- output += ` ${staticPrefix}${item.name}: ${item.returnType};\n\n`;
- }
-
- return output;
-}
-
-export function generateClassDeclaration(classDoc, organizedData) {
-
-
- let output = '';
-
- if (classDoc.description || classDoc.tags?.length > 0) {
- output += '/**\n';
- const description = extractDescription(classDoc.description);
- if (description) {
- output += formatJSDocComment(description) + '\n';
- }
- if (classDoc.tags) {
- if (description) {
- output += ' *\n';
- }
- classDoc.tags.forEach(tag => {
- if (tag.description) {
- const tagDesc = extractDescription(tag.description);
- output += formatJSDocComment(`@${tag.title} ${tagDesc}`, 0) + '\n';
- }
- });
- }
- output += ' */\n';
- }
-
- const parentClass = classDoc.extends;
- const extendsClause = parentClass ? ` extends ${parentClass}` : '';
-
- const fullClassName = normalizeClassName(classDoc.name);
- const classDocName = fullClassName.replace('p5.', '');
- output += `class ${classDocName}${extendsClause} {\n`;
-
- if (classDoc.params?.length > 0) {
- output += ' constructor(';
- output += classDoc.params
- .map(param => generateParamDeclaration(param))
- .join(', ');
- output += ');\n\n';
- }
-
- const classItems = organizedData.classitems.filter(item =>
- item.class === fullClassName ||
- item.class === fullClassName.replace('p5.', '')
- );
- const staticItems = classItems.filter(item => item.isStatic);
- const instanceItems = classItems.filter(item => !item.isStatic);
-
- staticItems.forEach(item => {
- output += generateMethodDeclarations(item, true);
- });
-
- instanceItems.forEach(item => {
- output += generateMethodDeclarations(item, false);
- });
-
- output += '}\n\n';
- return output;
-}
-
-function formatJSDocComment(text, indentLevel = 0) {
- if (!text) return '';
- const indent = ' '.repeat(indentLevel);
-
- const lines = text
- .split('\n')
- .map(line => line.trim())
- .reduce((acc, line) => {
- // If we're starting and line is empty, skip it
- if (acc.length === 0 && line === '') return acc;
- // If we have content and hit an empty line, keep one empty line
- if (acc.length > 0 && line === '' && acc[acc.length - 1] === '') return acc;
- acc.push(line);
- return acc;
- }, [])
- .filter((line, i, arr) => i < arr.length - 1 || line !== ''); // Remove trailing empty line
-
- return lines
- .map(line => `${indent} * ${line}`)
- .join('\n');
-}
-function groupByFile(items) {
- const fileGroups = new Map();
-
- items.forEach(item => {
- if (!item.context || !item.context.file) return;
-
- const filePath = item.context.file;
- if (!fileGroups.has(filePath)) {
- fileGroups.set(filePath, []);
- }
- fileGroups.get(filePath).push(item);
- });
-
- return fileGroups;
-}
diff --git a/utils/patch.mjs b/utils/patch.mjs
index 9aeee1b6d0..277d7808a8 100644
--- a/utils/patch.mjs
+++ b/utils/patch.mjs
@@ -1,41 +1,167 @@
import fs from 'fs';
-const replace = (path, src, dest) => {
- try {
- const data = fs
- .readFileSync(path, { encoding: 'utf-8' })
- .replace(src, dest);
- fs.writeFileSync(path, data);
- } catch (err) {
- console.error(err);
- }
-};
-
-replace(
- './types/core/structure.d.ts',
- 'function p5(sketch: object, node: string | HTMLElement): void;',
- 'function p5: typeof p5'
-);
-
-replace(
- './types/webgl/p5.Geometry.d.ts',
- 'constructor(detailX?: number, detailY?: number, callback?: function);',
- `constructor(
- detailX?: number,
- detailY?: number,
- callback?: (this: {
- detailY: number,
- detailX: number,
- vertices: p5.Vector[],
- uvs: number[]
- }) => void);`
-);
-
-// https://github.com/p5-types/p5.ts/issues/31
-replace(
- './types/math/random.d.ts',
- 'function random(choices: Array): any;',
- 'function random(choices: T[]): T;'
-);
+export function applyPatches() {
+ const cache = {};
+ const patched = {};
+
+ const replace = (path, src, dest) => {
+ if (Array.isArray(path)) {
+ path.forEach(path => replace(path, src, dest));
+ return;
+ }
+ try {
+ if (!path.startsWith("types/"))
+ path = "types/" + path;
+
+ const before = patched[path] ??
+ (cache[path] ??= fs.readFileSync("./" + path, { encoding: 'utf-8' }));
+ const after = before.replaceAll(src, dest);
+
+ if (after !== before)
+ patched[path] = after;
+ else
+ console.error(`A patch failed in ${path}:\n -${src}\n +${dest}`);
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ // TODO: Handle this better in the docs instead of patching
+ replace(
+ "p5.d.ts",
+ "constructor(detailX?: number, detailY?: number, callback?: Function);",
+ `constructor(
+ detailX?: number,
+ detailY?: number,
+ callback?: (this: Geometry) => void);`
+ );
+
+ // https://github.com/p5-types/p5.ts/issues/31
+ // #todo: add readonly to appropriate array params, either here or in doc comments
+ replace(
+ ["p5.d.ts", "global.d.ts"],
+ "random(choices: any[]): any;",
+ "random(choices: readonly T[]): T;"
+ );
+
+ replace(
+ 'p5.d.ts',
+ 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): object[][];',
+ 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): { x: number; y: number; alpha: number }[][];',
+ );
+
+ replace(
+ 'p5.d.ts',
+ 'class Renderer extends Element {}',
+ `class Renderer extends Element {
+ elt: HTMLCanvasElement;
+ }`
+ );
+
+ replace(
+ 'p5.d.ts',
+ 'class MediaElement extends p5.Element {',
+ `class MediaElement extends Element {
+ elt: HTMLAudioElement | HTMLVideoElement;
+ `
+ );
+ replace(
+ 'p5.d.ts',
+ 'class __Graphics extends p5.Element {',
+ `class __Graphics extends p5.Element {
+ elt: HTMLCanvasElement;
+ `,
+ );
+
+ // Type .elt more specifically for audio and video elements
+ replace(
+ 'p5.d.ts',
+ `class MediaElement extends Element {
+ elt: HTMLAudioElement | HTMLVideoElement;`,
+ `class MediaElement extends Element {
+ elt: T;`,
+ );
+ replace(
+ ['p5.d.ts', 'global.d.ts'],
+ /createAudio\(src\?: string \| string\[\], callback\?: Function\): ([pP]5)\.MediaElement;/g,
+ 'createAudio(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;',
+ );
+ replace(
+ ['p5.d.ts', 'global.d.ts'],
+ /createVideo\(src\?: string \| string\[\], callback\?: Function\): ([pP]5)\.MediaElement;/g,
+ 'createVideo(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;',
+ );
+
+ // More callback types
+ replace(
+ ['p5.d.ts', 'global.d.ts'],
+ /createFileInput\(callback: Function, multiple\?: boolean\): ([pP]5)\.Element;/g,
+ 'createFileInput(callback: (input: $1.File) => any, multiple?: boolean): $1.Element;',
+ );
+ replace(
+ ['p5.d.ts', 'global.d.ts'],
+ /loadFont\((.+), successCallback\?: Function, (.+)\): Promise\<([pP]5)\.Font\>;/g,
+ 'loadFont($1, successCallback?: (font: $3.Font) => any, $2): Promise<$3.Font>;'
+ );
+
+ // Type returned objects
+ replace(
+ 'p5.d.ts',
+ 'calculateBoundingBox(): object;',
+ 'calculateBoundingBox(): { min: p5.Vector; max: p5.Vector; size: p5.Vector; offset: p5.Vector };'
+ );
+ replace(
+ 'p5.d.ts',
+ 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): object;',
+ 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };',
+ );
+ replace(
+ 'p5.d.ts',
+ 'textBounds(str: string, x: number, y: number, width?: number, height?: number): object;',
+ 'textBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };',
+ );
+
+ // Document Typr
+ replace(
+ 'p5.d.ts',
+ 'class Font {',
+ `class Font {
+ /** The CSS name for the font. */
+ name: string;
+
+ /** The CSS FontFace definition for the font. */
+ face: FontFace;
+
+ /** Typr data for the font. */
+ data?: {
+ _data: Uint8Array;
+ GSUB: Record;
+ 'OS/2': Record;
+ cmap: {
+ ids: Record;
+ tables: Array>;
+ off: number;
+ };
+ glyf: Array;
+ head: Record;
+ hhea: Record;
+ htmx: Record;
+ loca: Array;
+ maxp: Record;
+ name: Record;
+ post: Record;
+ };
+ `
+ );
+
+ for (const [path, data] of Object.entries(patched)) {
+ try {
+ console.log(`Patched ${path}`);
+ fs.writeFileSync("./" + path, data);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+}
diff --git a/utils/shared-helpers.mjs b/utils/shared-helpers.mjs
new file mode 100644
index 0000000000..a4681c473d
--- /dev/null
+++ b/utils/shared-helpers.mjs
@@ -0,0 +1,153 @@
+// Shared helper functions used by both convert.mjs and typescript.mjs
+
+function getEntries(entry) {
+ return [
+ entry,
+ ...getAllEntries(entry.members?.global || []),
+ ...getAllEntries(entry.members?.inner || []),
+ ...getAllEntries(entry.members?.instance || []),
+ ...getAllEntries(entry.members?.events || []),
+ ...getAllEntries(entry.members?.static || [])
+ ];
+}
+
+export function getAllEntries(arr = []) {
+ return arr.flatMap(entry => entry ? getEntries(entry) : []);
+}
+
+export function descriptionString(node, parent) {
+ if (!node) {
+ return '';
+ } else if (node.type === 'text') {
+ return node.value;
+ } else if (node.type === 'paragraph') {
+ const content = node.children.map(n => descriptionString(n, node)).join('');
+ if (parent && parent.children.length === 1) return content;
+ return '' + content + '
\n';
+ } else if (node.type === 'code') {
+ let classes = [];
+ let attrs = '';
+ if (node.lang) {
+ classes.push(`language-${node.lang}`);
+ }
+ if (node.meta) {
+ classes.push(node.meta);
+ }
+ if (classes.length > 0) {
+ attrs=` class="${classes.join(' ')}"`;
+ }
+ return `${node.value}
`;
+ } else if (node.type === 'inlineCode') {
+ return '' + node.value + '
';
+ } else if (node.type === 'list') {
+ const tag = node.type === 'ordered' ? 'ol' : 'ul';
+ return `<${tag}>` + node.children.map(n => descriptionString(n, node)).join('') + `${tag}>`;
+ } else if (node.type === 'listItem') {
+ return '' + node.children.map(n => descriptionString(n, node)).join('') + ' ';
+ } else if (node.value) {
+ return node.value;
+ } else if (node.children) {
+ return node.children.map(n => descriptionString(n, node)).join('');
+ } else {
+ return '';
+ }
+}
+
+// TypeScript-specific version without HTML tags
+export function descriptionStringForTypeScript(node, parent) {
+ if (!node) {
+ return '';
+ } else if (node.type === 'text') {
+ return node.value;
+ } else if (node.type === 'paragraph') {
+ const content = node.children.map(n => descriptionStringForTypeScript(n, node)).join('');
+ return content + '\n\n'; // Skip HTML tags for TypeScript
+ } else if (node.type === 'code') {
+ return `\`${node.value}\``;
+ } else if (node.type === 'inlineCode') {
+ return `\`${node.value}\``;
+ } else if (node.type === 'list') {
+ return node.children.map(n => descriptionStringForTypeScript(n, node)).join('') + '\n';
+ } else if (node.type === 'listItem') {
+ return '- ' + node.children.map(n => descriptionStringForTypeScript(n, node)).join('') + '\n';
+ } else if (node.value) {
+ return node.value;
+ } else if (node.children) {
+ return node.children.map(n => descriptionStringForTypeScript(n, node)).join('');
+ } else {
+ return '';
+ }
+}
+
+export function typeObject(node) {
+ if (!node) return {};
+
+ if (node.type === 'OptionalType') {
+ return { optional: 1, ...typeObject(node.expression) };
+ } else if (node.type === 'UnionType') {
+ const names = node.elements.map(n => typeObject(n).type);
+ return {
+ type: names.join('|')
+ };
+ } else if (node.type === 'TypeApplication') {
+ const { type: typeName } = typeObject(node.expression);
+ if (
+ typeName === 'Array' &&
+ node.applications.length === 1
+ ) {
+ return {
+ type: `${typeObject(node.applications[0]).type}[]`
+ };
+ }
+ const args = node.applications.map(n => typeObject(n).type);
+ return {
+ type: `${typeName}<${args.join(', ')}>`
+ };
+ } else if (node.type === 'UndefinedLiteral') {
+ return { type: 'undefined' };
+ } else if (node.type === 'FunctionType') {
+ let signature = `function(${node.params.map(p => typeObject(p).type).join(', ')})`;
+ if (node.result) {
+ signature += `: ${typeObject(node.result).type}`;
+ }
+ return { type: signature };
+ } else if (node.type === 'ArrayType') {
+ return { type: `[${node.elements.map(e => typeObject(e).type).join(', ')}]` };
+ } else if (node.type === 'RestType') {
+ return { type: typeObject(node.expression).type, rest: true };
+ } else {
+ // TODO
+ // - handle record types
+ return { type: node.name };
+ }
+}
+
+export function getParams(entry) {
+ // Documentation.js seems to try to grab params from the function itself in
+ // the code if we don't document all the parameters. This messes with our
+ // manually-documented overloads. Instead of using the provided entry.params
+ // array, we'll instead only rely on manually included @param tags.
+ //
+ // However, the tags don't include a tree-structured description field, and
+ // instead convert it to a string. We want a slightly different conversion to
+ // string, so we match these params to the Documentation.js-provided `params`
+ // array and grab the description from those.
+ return (entry.tags || [])
+
+ // Filter out the nested parameters (eg. options.extrude),
+ // to be treated as part of parent parameters (eg. options)
+ // and not separate entries
+ .filter(t => t.title === 'param' && !t.name.includes('.'))
+ .map(node => {
+ const param = (entry.params || [])
+ .find(param => param.name === node.name);
+ return {
+ ...node,
+ description: param?.description || {
+ type: 'html',
+ value: node.description
+ },
+ properties: param?.properties // Preserve properties array for nested object parameters
+ };
+ });
+}
\ No newline at end of file
diff --git a/utils/typescript.mjs b/utils/typescript.mjs
new file mode 100644
index 0000000000..9d9841100a
--- /dev/null
+++ b/utils/typescript.mjs
@@ -0,0 +1,781 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { processData } from './data-processor.mjs';
+import { descriptionStringForTypeScript } from './shared-helpers.mjs';
+import { applyPatches } from './patch.mjs';
+import { strandsBuiltinFunctions as builtInGLSLFunctions } from '../src/strands/strands_builtins.js';
+import { DataType } from '../src/strands/ir_types.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Clear existing types directory and recreate it
+const typesDir = path.join(__dirname, '../types');
+if (fs.existsSync(typesDir)) {
+ fs.rmSync(typesDir, { recursive: true, force: true });
+}
+fs.mkdirSync(typesDir, { recursive: true });
+
+const rawData = JSON.parse(fs.readFileSync(path.join(__dirname, '../docs/data.json')));
+
+// Pre-build constants lookup
+import { getAllEntries } from './shared-helpers.mjs';
+const allRawData = getAllEntries(rawData);
+const constantsLookup = new Set();
+const typedefs = {};
+const mutableProperties = new Set(['disableFriendlyErrors']); // Properties that should be mutable, not constants
+allRawData.forEach(entry => {
+ if (entry.kind === 'constant' || entry.kind === 'typedef') {
+ constantsLookup.add(entry.name);
+ if (entry.kind === 'typedef') {
+ typedefs[entry.name] = entry.type;
+ }
+ }
+});
+
+// Process strands functions to extract p5 methods
+function processStrandsFunctions() {
+ const strandsMethods = [];
+
+ // Add ALL GLSL builtin functions (both isp5Function: true and false)
+ for (const [functionName, overloads] of Object.entries(builtInGLSLFunctions)) {
+ // Create method definition with simplified any types for all overloads
+ const method = {
+ name: functionName,
+ overloads: overloads.map(overload => ({
+ params: overload.params.map((paramType, index) => ({
+ name: `param${index}`,
+ type: { type: 'NameExpression', name: 'any' }, // Use 'any' for strands node types
+ optional: false
+ })),
+ return: {
+ type: { type: 'NameExpression', name: 'any' } // Return 'any' for strands nodes
+ }
+ })),
+ description: `GLSL built-in function ${functionName}`,
+ static: false
+ };
+
+ strandsMethods.push(method);
+ }
+
+ // Add uniform functions: uniformFloat, uniformVec2, etc.
+ const uniformMethods = [];
+ for (const type in DataType) {
+ if (type === 'defer') {
+ continue;
+ }
+
+ const typeInfo = DataType[type];
+ let pascalTypeName;
+
+ if (/^[ib]vec/.test(typeInfo.fnName)) {
+ pascalTypeName = typeInfo.fnName
+ .slice(0, 2).toUpperCase()
+ + typeInfo.fnName
+ .slice(2)
+ .toLowerCase();
+ } else {
+ pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase()
+ + typeInfo.fnName.slice(1).toLowerCase();
+ }
+
+ const uniformMethodName = `uniform${pascalTypeName}`;
+ const uniformMethod = {
+ name: uniformMethodName,
+ overloads: [{
+ params: [
+ {
+ name: 'name',
+ type: { type: 'NameExpression', name: 'String' },
+ optional: false
+ },
+ {
+ name: 'defaultValue',
+ type: { type: 'NameExpression', name: 'any' },
+ optional: true
+ }
+ ],
+ return: {
+ type: { type: 'NameExpression', name: 'any' }
+ }
+ }],
+ description: `Create a ${pascalTypeName} uniform variable`,
+ static: false
+ };
+
+ uniformMethods.push(uniformMethod);
+
+ // Add Vector aliases for Vec types
+ if (pascalTypeName.startsWith('Vec')) {
+ const vectorMethodName = `uniform${pascalTypeName.replace('Vec', 'Vector')}`;
+ const vectorMethod = {
+ ...uniformMethod,
+ name: vectorMethodName,
+ description: `Create a ${pascalTypeName.replace('Vec', 'Vector')} uniform variable`
+ };
+ uniformMethods.push(vectorMethod);
+ }
+ }
+
+ // Add type casting functions (DataType constructor functions)
+ const typeCastingMethods = [];
+ for (const type in DataType) {
+ if (type === 'defer' || !DataType[type].fnName) {
+ continue;
+ }
+
+ const typeInfo = DataType[type];
+ const castingMethod = {
+ name: typeInfo.fnName,
+ overloads: [{
+ params: [
+ {
+ name: 'value',
+ type: { type: 'NameExpression', name: 'any' },
+ optional: false
+ }
+ ],
+ return: {
+ type: { type: 'NameExpression', name: 'any' }
+ }
+ }],
+ description: `GLSL type constructor for ${typeInfo.fnName}`,
+ static: false
+ };
+
+ typeCastingMethods.push(castingMethod);
+ }
+
+ return [...strandsMethods, ...uniformMethods, ...typeCastingMethods];
+}
+
+// TypeScript-specific type conversion from raw type objects
+function convertTypeToTypeScript(typeNode, options = {}) {
+ if (!typeNode) return 'any';
+
+ // Validate that typeNode is always an object
+ if (typeof typeNode !== 'object' || Array.isArray(typeNode)) {
+ throw new Error(`convertTypeToTypeScript expects an object, got: ${typeof typeNode} - ${JSON.stringify(typeNode)}`);
+ }
+
+ const { currentClass = null, isInsideNamespace = false, inGlobalMode = false, isConstantDef = false } = options;
+
+ switch (typeNode.type) {
+ case 'NameExpression': {
+ const typeName = typeNode.name;
+
+ // Handle primitive types
+ const primitiveTypes = {
+ 'String': 'string',
+ 'Number': 'number',
+ 'Integer': 'number',
+ 'Boolean': 'boolean',
+ 'Void': 'void',
+ 'Object': 'object',
+ 'Any': 'any',
+ 'Array': 'any[]',
+ 'Promise': 'Promise',
+ 'Function': 'Function',
+ 'HTMLElement': 'HTMLElement',
+ 'Event': 'Event',
+ 'Request': 'Request'
+ };
+
+ if (primitiveTypes[typeName]) {
+ return primitiveTypes[typeName];
+ }
+
+ // Handle self-referential types within the same class
+ if (currentClass && (typeName === `p5.${currentClass}` || typeName === currentClass)) {
+ return currentClass;
+ }
+
+ // If we're inside the p5 namespace, remove p5. prefix from other p5 classes
+ if (isInsideNamespace && typeName.startsWith('p5.')) {
+ if (inGlobalMode) {
+ return 'P5.' + typeName.substring(3);
+ } else {
+ return typeName.substring(3);
+ }
+ }
+
+ // Check if this is a p5 constant - use typeof since they're defined as values
+ if (constantsLookup.has(typeName)) {
+ if (inGlobalMode) {
+ return `typeof P5.${typeName}`;
+ } else if (typedefs[typeName]) {
+ if (isConstantDef) {
+ return convertTypeToTypeScript(typedefs[typeName], options);
+ } else {
+ return `typeof p5.${typeName}`
+ }
+ } else {
+ return `Symbol`;
+ }
+ }
+
+ return typeName;
+ }
+
+ case 'TypeApplication': {
+ const baseTypeName = typeNode.expression.name;
+
+ if (baseTypeName === 'Array' && typeNode.applications.length === 1) {
+ const innerType = convertTypeToTypeScript(typeNode.applications[0], options);
+ return `${innerType}[]`;
+ }
+
+ // For generic types, use the base type name directly to avoid double conversion
+ const typeParams = typeNode.applications
+ .map(app => convertTypeToTypeScript(app, options))
+ .join(', ');
+ return `${baseTypeName}<${typeParams}>`;
+ }
+
+ case 'UnionType': {
+ const unionTypes = typeNode.elements
+ .map(el => convertTypeToTypeScript(el, options))
+ .join(' | ');
+ return unionTypes;
+ }
+
+ case 'OptionalType':
+ return convertTypeToTypeScript(typeNode.expression, options);
+
+ case 'AllLiteral':
+ return 'any';
+
+ case 'RecordType':
+ return 'object';
+
+ case 'NumericLiteralType':
+ return `${typeNode.value}`;
+
+ case 'StringLiteralType':
+ return `'${typeNode.value}'`;
+
+ case 'NullLiteral':
+ return 'null';
+
+ case 'UndefinedLiteral':
+ return 'undefined';
+
+ case 'ArrayType': {
+ const innerTypes = typeNode.elements.map(e => convertTypeToTypeScript(e, options));
+ return `[${innerTypes.join(', ')}]`;
+ }
+
+ case 'RestType':
+ return `${convertTypeToTypeScript(typeNode.expression, options)}[]`;
+
+ case 'FunctionType': {
+ const params = (typeNode.params || [])
+ .map((param, i) => {
+ const paramType = convertTypeToTypeScript(param, options);
+ return `arg${i}: ${paramType}`;
+ })
+ .join(', ');
+
+ const returnType = typeNode.result
+ ? convertTypeToTypeScript(typeNode.result, options)
+ : 'void';
+ return `(${params}) => ${returnType}`;
+ }
+
+ default:
+ return 'any';
+ }
+}
+
+// Strategy for TypeScript output
+const typescriptStrategy = {
+ shouldSkipEntry: (entry, context) => {
+ // Skip Foundation module for TypeScript output
+ return context.module === 'Foundation';
+ },
+
+ processDescription: (desc) => descriptionStringForTypeScript(desc),
+
+ processType: (type, param) => {
+ // Return an object with the original type preserved
+ // This matches the expected data structure from the data processor
+ const result = {
+ type: type, // Keep the original raw type object
+ originalType: type // Also store it here for clarity
+ };
+
+ // Extract optional flag from OptionalType
+ if (type?.type === 'OptionalType') {
+ result.optional = true;
+ }
+
+ // Extract rest flag from RestType
+ if (type?.type === 'RestType') {
+ result.rest = true;
+ }
+
+ // Preserve properties array for nested object parameters
+ if (param && param.properties) {
+ result.properties = param.properties;
+ }
+
+ return result;
+ }
+};
+
+const processed = processData(rawData, typescriptStrategy);
+
+function normalizeIdentifier(name) {
+ return (
+ '0123456789'.includes(name[0]) ||
+ name === 'class'
+ ) ? '$' + name : name;
+}
+
+function formatJSDocComment(text, indentLevel = 0) {
+ if (!text) return '';
+ const indent = ' '.repeat(indentLevel);
+
+ const lines = text
+ .split('\n')
+ .map(line => line.trim())
+ .reduce((acc, line) => {
+ if (acc.length === 0 && line === '') return acc;
+ if (acc.length > 0 && line === '' && acc[acc.length - 1] === '') return acc;
+ acc.push(line);
+ return acc;
+ }, [])
+ .filter((line, i, arr) => i < arr.length - 1 || line !== '');
+
+ return lines
+ .map(line => `${indent} * ${line}`)
+ .join('\n');
+}
+
+function generateObjectInterface(param, allParams, options = {}) {
+ // Check if this is an object parameter (either required or optional)
+ const isObjectParam = param.type && (
+ (param.type.type === 'OptionalType' && param.type.expression?.name === 'Object') ||
+ (param.type.type === 'NameExpression' && param.type.name === 'Object')
+ );
+
+ if (!isObjectParam || !param.name) {
+ return null;
+ }
+
+ let nestedParams = [];
+
+
+ // First, check if the parameter has a properties array (JSDoc properties field)
+ if (param.properties && Array.isArray(param.properties)) {
+ nestedParams = param.properties.filter(prop =>
+ prop.name && prop.name.startsWith(param.name + '.')
+ );
+ }
+
+ // Fallback: Look for nested parameters with dot notation in allParams
+ if (nestedParams.length === 0) {
+ nestedParams = allParams.filter(p =>
+ p.name && p.name.startsWith(param.name + '.') && p.name !== param.name
+ );
+ }
+
+ if (nestedParams.length === 0) {
+ return null;
+ }
+
+ // Generate interface properties
+ const properties = nestedParams.map(nestedParam => {
+ const propName = nestedParam.name.substring(param.name.length + 1); // Remove 'paramName.' prefix
+ const propType = nestedParam.type ? convertTypeToTypeScript(nestedParam.type, options) : 'any';
+ // Properties are optional if they have a default value or are explicitly marked as optional
+ const isOptional = nestedParam.optional || nestedParam.type?.type === 'OptionalType' || nestedParam.default !== undefined;
+ return `${propName}${isOptional ? '?' : ''}: ${propType}`;
+ });
+
+ return `{ ${properties.join('; ')} }`;
+}
+
+function generateParamDeclaration(param, options = {}, allParams = []) {
+ if (!param) return '';
+
+ const name = normalizeIdentifier(param.name);
+
+ // Check if this is an object parameter that we can generate a better interface for
+ const objectInterface = generateObjectInterface(param, allParams, options);
+
+ // Convert the type - should always be an object
+ let type = 'any';
+ if (objectInterface) {
+ type = objectInterface;
+ } else if (param.type) {
+ type = convertTypeToTypeScript(param.type, options);
+ }
+
+ const isOptional = param.optional;
+
+ let prefix = '';
+ if (param.rest) {
+ prefix = '...';
+ }
+
+ return `${prefix}${name}${isOptional ? '?' : ''}: ${type}`;
+}
+
+function generateMethodDeclaration(method, options = {}) {
+ let output = '';
+ const { globalFunction = false } = options;
+
+ const indent = globalFunction ? '' : ' ';
+ const commentIndent = globalFunction ? 0 : 2;
+
+ if (method.description) {
+ output += `${indent}/**\n`;
+ output += formatJSDocComment(method.description, commentIndent) + '\n';
+
+ // Add param docs from first overload
+ if (method.overloads?.[0]?.params) {
+ method.overloads[0].params.forEach(param => {
+ if (param.description) {
+ output += formatJSDocComment(`@param ${param.name} ${param.description}`, commentIndent) + '\n';
+ }
+ });
+ }
+
+ // Add return docs
+ if (method.return?.description) {
+ output += formatJSDocComment(`@returns ${method.return.description}`, commentIndent) + '\n';
+ }
+
+ output += `${indent} */\n`;
+ }
+
+ const staticPrefix = method.static ? 'static ' : '';
+ const declarationPrefix = globalFunction ? 'function ' : `${indent}${staticPrefix}`;
+
+ // Generate overload declarations
+ if (method.overloads && method.overloads.length > 0) {
+ method.overloads.forEach(overload => {
+ const params = (overload.params || [])
+ .map(param => generateParamDeclaration(param, options, overload.params))
+ .join(', ');
+
+ let returnType = 'void';
+ if (method.chainable && !globalFunction && options.currentClass !== 'p5') {
+ returnType = options.currentClass || 'this';
+ // TODO: Decide what should be chainable. Many of these are accidental / not thought through
+ } else if (overload.return && overload.return.type) {
+ returnType = convertTypeToTypeScript(overload.return.type, options);
+ } else if (method.return && method.return.type) {
+ returnType = convertTypeToTypeScript(method.return.type, options);
+ }
+
+ output += `${declarationPrefix}${method.name}(${params}): ${returnType};\n`;
+ });
+ }
+
+ output += '\n';
+ return output;
+}
+
+function generateClassDeclaration(classData) {
+ let output = '';
+ const className = classData.name.startsWith('p5.') ? classData.name.substring(3) : classData.name;
+ const actualClassName = className === 'Graphics' ? '__Graphics' : className;
+
+ if (classData.description) {
+ output += ' /**\n';
+ output += formatJSDocComment(classData.description, 2) + '\n';
+ output += ' */\n';
+ }
+
+ const extendsClause = classData.extends ? ` extends ${classData.extends}` : '';
+ output += ` class ${actualClassName}${extendsClause} {\n`;
+
+ // Constructor
+ if (classData.params?.length > 0) {
+ output += ' constructor(';
+ output += classData.params
+ .map(param => generateParamDeclaration(param, { currentClass: className, isInsideNamespace: true }, classData.params))
+ .join(', ');
+ output += ');\n\n';
+ }
+
+ const options = { currentClass: className, isInsideNamespace: true };
+ const originalClassName = classData.name;
+
+ // Class methods
+ const classMethodsList = Object.values(processed.classMethods[originalClassName] || {});
+ const methodNames = new Set(classMethodsList.map(method => method.name));
+
+ // Class properties
+ const classProperties = processed.classitems.filter(item =>
+ item.class === originalClassName && item.itemtype === 'property'
+ );
+
+ classProperties.forEach(prop => {
+ // Skip properties that conflict with method names
+ if (methodNames.has(prop.name)) {
+ return;
+ }
+
+ if (prop.description) {
+ output += ' /**\n';
+ output += formatJSDocComment(prop.description, 4) + '\n';
+ output += ' */\n';
+ }
+ const type = convertTypeToTypeScript(prop.type, options);
+ output += ` ${prop.name}: ${type};\n\n`;
+ });
+ const staticMethods = classMethodsList.filter(method => method.static);
+ const instanceMethods = classMethodsList.filter(method => !method.static);
+
+ staticMethods.forEach(method => {
+ output += generateMethodDeclaration(method, options);
+ });
+
+ instanceMethods.forEach(method => {
+ output += generateMethodDeclaration(method, options);
+ });
+
+ output += ' }\n\n';
+
+ // Add type alias for Graphics
+ if (className === 'Graphics') {
+ output += ' type Graphics = __Graphics & p5;\n\n';
+ }
+
+ return output;
+}
+
+// Generate TypeScript definitions
+function generateTypeDefinitions() {
+ let output = '// This file is auto-generated from JSDoc documentation\n\n';
+
+ // First, define all constants at the top level with their actual values
+ const seenConstants = new Set();
+ const p5Constants = processed.classitems.filter(item => {
+ if (item.class === 'p5' && item.itemtype === 'property' && item.name in processed.consts) {
+ // Skip defineProperty, undefined and avoid duplicates
+ if (item.name === 'defineProperty' || !item.name) {
+ return false;
+ }
+ if (seenConstants.has(item.name)) {
+ return false;
+ }
+ seenConstants.add(item.name);
+ return true;
+ }
+ return false;
+ });
+
+ p5Constants.forEach(constant => {
+ if (constant.description) {
+ output += '/**\n';
+ output += formatJSDocComment(constant.description, 0) + '\n';
+ output += ' */\n';
+ }
+ const type = convertTypeToTypeScript(constant.type, { isInsideNamespace: false, isConstantDef: true });
+ const isMutable = mutableProperties.has(constant.name);
+ const declaration = isMutable ? 'declare let' : 'declare const';
+ output += `${declaration} ${constant.name}: ${type};\n\n`;
+ // Duplicate with a private identifier so we can re-export in the namespace later
+ output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`;
+ });
+
+ // Generate main p5 class
+ output += 'declare class p5 {\n';
+ output += ' constructor(sketch?: (p: p5) => void, node?: HTMLElement, sync?: boolean);\n\n';
+
+ const p5Options = { currentClass: 'p5', isInsideNamespace: false };
+
+ // Generate p5 static methods
+ const p5StaticMethods = Object.values(processed.classMethods.p5 || {}).filter(method => method.static);
+ p5StaticMethods.forEach(method => {
+ output += generateMethodDeclaration(method, p5Options);
+ });
+
+ // Generate p5 instance methods
+ const p5InstanceMethods = Object.values(processed.classMethods.p5 || {}).filter(method => !method.static);
+ p5InstanceMethods.forEach(method => {
+ output += generateMethodDeclaration(method, p5Options);
+ });
+
+ // Add strands functions to p5 instance
+ const strandsMethods = processStrandsFunctions();
+ strandsMethods.forEach(method => {
+ output += generateMethodDeclaration(method, p5Options);
+ });
+
+ // Add constants as both instance and static properties (referencing the top-level constants)
+ p5Constants.forEach(constant => {
+ const isMutable = mutableProperties.has(constant.name);
+ const readonly = isMutable ? '' : 'readonly ';
+ output += ` ${readonly}${constant.name}: typeof ${constant.name};\n`;
+ });
+
+ output += '}\n\n';
+
+ output += 'declare const __p5: typeof p5;\n\n';
+
+ // Generate p5 namespace
+ output += 'declare namespace p5 {\n';
+ output += ' const p5: typeof __p5;\n';
+
+ output += '\n';
+
+
+ p5Constants.forEach(constant => {
+ output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`;
+ });
+
+ output += '\n';
+
+ // Generate other classes in namespace
+ Object.values(processed.classes).forEach(classData => {
+ if (classData.name !== 'p5') {
+ output += generateClassDeclaration(classData);
+ }
+ });
+
+ // Generate placeholder types for private classes that we need to be able to
+ // reference, but have no public APIs
+ const privateClasses = ['Renderer', 'Renderer2D', 'RendererGL', 'FramebufferTexture', 'Texture', 'Quat'];
+ // Define base classes for private classes, if they should extend something
+ const privateClassBases = { Renderer: 'Element' };
+ for (const className of privateClasses) {
+ const baseClass = privateClassBases[className];
+ if (baseClass) {
+ output += ` class ${className} extends ${baseClass} {}\n`;
+ } else {
+ output += ` class ${className} {}\n`;
+ }
+ }
+
+ output += '}\n\n';
+
+ // Export declarations
+ output += 'export default p5;\n';
+ output += 'export as namespace p5;\n';
+
+ const instanceDefinitions = output;
+
+ let globalDefinitions = `// This file is auto-generated from JSDoc documentation
+
+import P5 from './p5';
+
+declare global {
+interface Window {
+
+p5: P5;
+`;
+
+ p5Constants.forEach(constant => {
+ if (constant.description) {
+ globalDefinitions += '/**\n';
+ globalDefinitions += formatJSDocComment(constant.description, 0) + '\n';
+ globalDefinitions += ' */\n';
+ }
+ globalDefinitions += `${constant.name}: typeof P5.${constant.name};\n\n`;
+ });
+
+ const globalP5Methods = Object.values(processed.classMethods.p5 || {})
+ .filter(method => !method.static && method.name !== 'p5');
+ globalP5Methods.forEach(method => {
+ globalDefinitions += generateMethodDeclaration(method, { currentClass: 'p5', isInsideNamespace: true, inGlobalMode: true });
+ });
+
+ // Add strands functions to global scope
+ const conflictingDOMFunctions = ['length']; // Add other conflicting function names here as needed
+ strandsMethods.forEach(method => {
+ if (!conflictingDOMFunctions.includes(method.name)) {
+ globalDefinitions += generateMethodDeclaration(method, { currentClass: 'p5', isInsideNamespace: true, inGlobalMode: true });
+ }
+ });
+
+ globalDefinitions += '}\n';
+
+ // Add global p5 namespace with all class types and constants
+ globalDefinitions += '\nnamespace p5 {\n';
+
+ // Add all constants
+ p5Constants.forEach(constant => {
+ const isMutable = mutableProperties.has(constant.name);
+ const declaration = isMutable ? 'let' : 'const';
+ globalDefinitions += ` ${declaration} ${constant.name}: typeof P5.${constant.name};\n`;
+ });
+
+ globalDefinitions += '\n';
+
+ // Add all real classes as both types and constructors
+ Object.values(processed.classes).forEach(classData => {
+ if (classData.name !== 'p5') {
+ const className = classData.name.startsWith('p5.') ? classData.name.substring(3) : classData.name;
+ // For Graphics, use __Graphics for constructor
+ if (className === 'Graphics') {
+ globalDefinitions += ` type ${className} = P5.${className};\n`;
+ globalDefinitions += ` const ${className}: typeof P5.__${className};\n`;
+ } else {
+ globalDefinitions += ` type ${className} = P5.${className};\n`;
+ globalDefinitions += ` const ${className}: typeof P5.${className};\n`;
+ }
+ }
+ });
+
+ // Add private classes
+ for (const className of privateClasses) {
+ globalDefinitions += ` type ${className} = P5.${className};\n`;
+ globalDefinitions += ` const ${className}: typeof P5.${className};\n`;
+ }
+
+ globalDefinitions += '}\n\n';
+
+ // Also declare constants in global scope (deduplicated)
+ const alreadyDeclaredConstants = new Set();
+ p5Constants.forEach(constant => {
+ if (alreadyDeclaredConstants.has(constant.name)) {
+ return; // Skip duplicates
+ }
+ if (constant.name === 'defineProperty' || !constant.name) {
+ return; // Skip problematic constants
+ }
+ alreadyDeclaredConstants.add(constant.name);
+
+ if (constant.description) {
+ globalDefinitions += '/**\n';
+ globalDefinitions += formatJSDocComment(constant.description, 0) + '\n';
+ globalDefinitions += ' */\n';
+ }
+ globalDefinitions += `const ${constant.name}: typeof P5.${constant.name};\n\n`;
+ });
+
+ // Also declare functions in global scope
+ globalP5Methods.forEach(method => {
+ globalDefinitions += generateMethodDeclaration(method, { currentClass: 'p5', isInsideNamespace: true, inGlobalMode: true, globalFunction: true });
+ });
+
+ // Add strands functions as global functions
+ strandsMethods.forEach(method => {
+ if (!conflictingDOMFunctions.includes(method.name)) {
+ globalDefinitions += generateMethodDeclaration(method, { currentClass: 'p5', isInsideNamespace: true, inGlobalMode: true, globalFunction: true });
+ }
+ });
+
+ globalDefinitions += '}\n\n';
+
+ return { instanceDefinitions, globalDefinitions };
+}
+
+// Generate and write TypeScript definitions
+const { instanceDefinitions, globalDefinitions } = generateTypeDefinitions();
+
+fs.writeFileSync(path.join(__dirname, '../types/p5.d.ts'), instanceDefinitions);
+fs.writeFileSync(path.join(__dirname, '../types/global.d.ts'), globalDefinitions);
+
+console.log('TypeScript definitions generated successfully!');
+
+// Apply patches
+console.log('Applying TypeScript patches...');
+applyPatches();
\ No newline at end of file
diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs
index 3b8cc242f7..21d479b2d8 100644
--- a/vitest.workspace.mjs
+++ b/vitest.workspace.mjs
@@ -29,7 +29,8 @@ export default defineWorkspace([
exclude: [
'./test/unit/spec.js',
'./test/unit/assets/**/*',
- './test/unit/visual/visualTest.js'
+ './test/unit/visual/visualTest.js',
+ './test/types/**/*'
],
testTimeout: 1000,
globals: true,