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 *
@@ -241,6 +242,7 @@ function setting(p5, fn){ * @method clip * @param {Function} callback a function that draws the mask shape. * @param {Object} [options] an object containing clip settings. + * @param {Boolean} [options.invert=false] Whether or not to invert the mask. * * @example *
@@ -819,7 +821,7 @@ function setting(p5, fn){ * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode either RGB, HSB, HSL, * or one of the extended modes described above. * @param {Number} [max] range for all values. - * @chainable + * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode. * * @example *
@@ -1159,7 +1161,6 @@ function setting(p5, fn){ * *
*/ - /** * @method colorMode * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode @@ -1171,7 +1172,11 @@ function setting(p5, fn){ * depending on the current color mode. * @param {Number} [maxA] range for the alpha. * - * @return {String} The current color mode. + * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode. + */ + /** + * @method colorMode + * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode. */ fn.colorMode = function(mode, max1, max2, max3, maxA) { // p5._validateParameters('colorMode', arguments); diff --git a/src/core/constants.js b/src/core/constants.js index 942b48c9ad..c203106730 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -57,7 +57,7 @@ export const P2DHDR = 'p2d-hdr'; * * To learn more about WEBGL mode, check out all the interactive WEBGL tutorials in the "Tutorials" section of this website, or read the wiki article "Getting started with WebGL in p5". * - * @typedef {unique symbol} WEBGL + * @typedef {'webgl'} WEBGL * @property {WEBGL} WEBGL * @final */ @@ -66,7 +66,7 @@ export const WEBGL = 'webgl'; * One of the two possible values of a WebGL canvas (either WEBGL or WEBGL2), * which can be used to determine what capabilities the rendering environment * has. - * @typedef {unique symbol} WEBGL2 + * @typedef {'webgl2'} WEBGL2 * @property {WEBGL2} WEBGL2 * @final */ @@ -584,92 +584,15 @@ export const TAU = _PI * 2; export const TWO_PI = _PI * 2; /** - * 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 {unique symbol} DEGREES - * @property {DEGREES} DEGREES + * @property {Number} DEG_TO_RAD * @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.'); - * } - * - *
*/ -// export const DEGREES = Symbol('degrees'); +export const DEG_TO_RAD = _PI / 180.0; /** - * 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 {unique symbol} RADIANS - * @property {RADIANS} RADIANS + * @property {Number} RAD_TO_DEG * @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.'); - * } - * - *
*/ -// export const RADIANS = Symbol('radians'); -export const DEG_TO_RAD = _PI / 180.0; export const RAD_TO_DEG = 180.0 / _PI; // SHAPE @@ -740,7 +663,7 @@ export const POINTS = 0x0000; */ export const LINES = 0x0001; /** - * @property {0x0003} LINE_STRIP + * @typedef {0x0003} LINE_STRIP * @property {LINE_STRIP} LINE_STRIP * @final */ @@ -831,7 +754,7 @@ export const PIE = 'pie'; export const PROJECT = 'square'; // PEND: careful this is counterintuitive /** * @typedef {'butt'} SQUARE - * @property {SQUERE} SQUARE + * @property {SQUARE} SQUARE * @final */ export const SQUARE = 'butt'; diff --git a/src/core/environment.js b/src/core/environment.js index 007ec18742..a3226aa77b 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -819,146 +819,12 @@ function environment(p5, fn, lifecycles){ this.windowHeight = getWindowHeight(); }; - /** - * A `Number` variable that stores the width of the canvas in pixels. - * - * `width`'s default value is 100. Calling - * createCanvas() or - * resizeCanvas() changes the value of - * `width`. Calling noCanvas() sets its value to - * 0. - * - * @example - *
- * - * function setup() { - * background(200); - * - * // Display the canvas' width. - * text(width, 42, 54); - * - * describe('The number 100 written in black on a gray square.'); - * } - * - *
- * - *
- * - * 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); - * } - * } - * - *
- * - * @property {Number} width - * @readOnly - */ Object.defineProperty(fn, 'width', { get(){ return this._renderer.width; } }); - /** - * A `Number` variable that stores the height of the canvas in pixels. - * - * `height`'s default value is 100. Calling - * createCanvas() or - * resizeCanvas() changes the value of - * `height`. Calling noCanvas() sets its value to - * 0. - * - * @example - *
- * - * function setup() { - * background(200); - * - * // Display the canvas' height. - * text(height, 42, 54); - * - * describe('The number 100 written in black on a gray square.'); - * } - * - *
- * - *
- * - * 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); - * } - * } - * - *
- * - * @property {Number} height - * @readOnly - */ Object.defineProperty(fn, 'height', { get(){ return this._renderer.height; @@ -1468,6 +1334,142 @@ function environment(p5, fn, lifecycles){ .multiplyAndNormalizePoint(screenPosition); return worldPosition; }; + + /** + * A `Number` variable that stores the width of the canvas in pixels. + * + * `width`'s default value is 100. Calling + * createCanvas() or + * resizeCanvas() changes the value of + * `width`. Calling noCanvas() sets its value to + * 0. + * + * @example + *
+ * + * function setup() { + * background(200); + * + * // Display the canvas' width. + * text(width, 42, 54); + * + * describe('The number 100 written in black on a gray square.'); + * } + * + *
+ * + *
+ * + * 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); + * } + * } + * + *
+ * + * @property {Number} width + * @readOnly + */ + + /** + * A `Number` variable that stores the height of the canvas in pixels. + * + * `height`'s default value is 100. Calling + * createCanvas() or + * resizeCanvas() changes the value of + * `height`. Calling noCanvas() sets its value to + * 0. + * + * @example + *
+ * + * function setup() { + * background(200); + * + * // Display the canvas' height. + * text(height, 42, 54); + * + * describe('The number 100 written in black on a gray square.'); + * } + * + *
+ * + *
+ * + * 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); + * } + * } + * + *
+ * + * @property {Number} height + * @readOnly + */ } export default environment; diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index d2c813a940..4a6b206ae6 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -137,6 +137,7 @@ function validateParams(p5, fn, lifecycles) { * parameters, and `?` is a shorthand for `Optional`. * * @method generateZodSchemasForFunc + * @private * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add` * @returns {z.ZodSchema} Zod schema */ diff --git a/src/core/main.js b/src/core/main.js index 526171150c..6a71cd2417 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -27,7 +27,7 @@ import * as constants from './constants'; * @param {function(p5)} sketch a closure that can set optional preload(), * setup(), and/or draw() properties on the * given p5 instance - * @param {HTMLElement} [node] element to attach canvas to + * @param {String|HTMLElement} [node] element to attach canvas to * @return {p5} a p5 instance */ class p5 { @@ -528,6 +528,24 @@ for (const k in constants) { p5.prototype[k] = constants[k]; } +import transform from './transform'; +import structure from './structure'; +import environment from './environment'; +import rendering from './rendering'; +import renderer from './p5.Renderer'; +import renderer2D from './p5.Renderer2D'; +import graphics from './p5.Graphics'; + +p5.registerAddon(transform); +p5.registerAddon(structure); +p5.registerAddon(environment); +p5.registerAddon(rendering); +p5.registerAddon(renderer); +p5.registerAddon(renderer2D); +p5.registerAddon(graphics); + +export default p5; + ////////////////////////////////////////////// // PUBLIC p5 PROPERTIES AND METHODS ////////////////////////////////////////////// @@ -757,20 +775,3 @@ for (const k in constants) { * *
*/ -import transform from './transform'; -import structure from './structure'; -import environment from './environment'; -import rendering from './rendering'; -import renderer from './p5.Renderer'; -import renderer2D from './p5.Renderer2D'; -import graphics from './p5.Graphics'; - -p5.registerAddon(transform); -p5.registerAddon(structure); -p5.registerAddon(environment); -p5.registerAddon(rendering); -p5.registerAddon(renderer); -p5.registerAddon(renderer2D); -p5.registerAddon(graphics); - -export default p5; diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index b831595344..bee903c6eb 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -341,6 +341,16 @@ class Graphics { * automatically match the graphics buffer and must be changed manually. * * @param {Object} [options] configuration options. + * @param {UNSIGNED_BYTE|FLOAT|HALF_FLOAT} [options.format=UNSIGNED_BYTE] The data format of the texture. + * @param {RGB|RGBA} [options.channels=RGBA] What color channels to include in the texture. + * @param {Boolean} [options.depth=true] Whether to store depth information in the framebuffer. + * @param {UNSIGNED_INT|FLOAT} [options.depthFormat=FLOAT] The format to store depth values in. + * @param {Boolean} [options.stencil=true] Whether to include a stencil buffer (required for clipping.) + * @param {Boolean|Number} [options.antialias] Whether to antialias when drawing to this framebuffer. Either a boolean, or the number of antialias samples to use. + * @param {Number} [options.width] The width of the framebuffer. By default, it will match the main canvas. + * @param {Number} [options.height] The height of the framebuffer. By default, it will match the main canvas. + * @param {Number} [options.density] The pixel density of the framebuffer. By default, it will match the main canvas. + * @param {LINEAR|NEAREST} [options.textureFiltering=LINEAR] The strategy used when reading values in the framebuffer in between pixels. * @return {p5.Framebuffer} new framebuffer. * * @example diff --git a/src/core/rendering.js b/src/core/rendering.js index a941c3bd1e..5c145b24f1 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -434,6 +434,16 @@ function rendering(p5, fn){ * * @method createFramebuffer * @param {Object} [options] configuration options. + * @param {UNSIGNED_BYTE|FLOAT|HALF_FLOAT} [options.format=UNSIGNED_BYTE] The data format of the texture. + * @param {RGB|RGBA} [options.channels=RGBA] What color channels to include in the texture. + * @param {Boolean} [options.depth=true] Whether to store depth information in the framebuffer. + * @param {UNSIGNED_INT|FLOAT} [options.depthFormat=FLOAT] The format to store depth values in. + * @param {Boolean} [options.stencil=true] Whether to include a stencil buffer (required for clipping.) + * @param {Boolean|Number} [options.antialias] Whether to antialias when drawing to this framebuffer. Either a boolean, or the number of antialias samples to use. + * @param {Number} [options.width] The width of the framebuffer. By default, it will match the main canvas. + * @param {Number} [options.height] The height of the framebuffer. By default, it will match the main canvas. + * @param {Number} [options.density] The pixel density of the framebuffer. By default, it will match the main canvas. + * @param {LINEAR|NEAREST} [options.textureFiltering=LINEAR] The strategy used when reading values in the framebuffer in between pixels. * @return {p5.Framebuffer} new framebuffer. * * @example @@ -640,7 +650,7 @@ function rendering(p5, fn){ * CanvasRenderingContext2D * object. * - * @property drawingContext + * @property {CanvasRenderingContext2D|WebGLRenderingContext|WebGL2RenderingContext} drawingContext * * @example *
diff --git a/src/core/transform.js b/src/core/transform.js index 2c802d3f69..8832d3a4ee 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -56,7 +56,7 @@ function transform(p5, fn){ * cause shapes to transform continuously. * * @method applyMatrix - * @param {Array} arr an array containing the elements of the transformation matrix. Its length should be either 6 (2D) or 16 (3D). + * @param {Array} arr an array containing the elements of the transformation matrix. Its length should be either 6 (2D) or 16 (3D). * @chainable * * @example diff --git a/src/dom/dom.js b/src/dom/dom.js index 48144c9198..d9dd5c9e9e 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -1871,7 +1871,7 @@ function dom(p5, fn){ * @method createFileInput * @param {Function} callback function to call once the file loads. * @param {Boolean} [multiple] allow multiple files to be selected. - * @return {p5.File} new p5.File object. + * @return {p5.Element} The new input element. * * @example *
diff --git a/src/dom/p5.Element.js b/src/dom/p5.Element.js index 765b7db267..242f80d343 100644 --- a/src/dom/p5.Element.js +++ b/src/dom/p5.Element.js @@ -2534,6 +2534,7 @@ function element(p5, fn){ * *
* + * @type {HTMLElement} * @property elt * @for p5.Element * @name elt diff --git a/src/dom/p5.File.js b/src/dom/p5.File.js index 7852d4f435..7c3f182635 100644 --- a/src/dom/p5.File.js +++ b/src/dom/p5.File.js @@ -155,7 +155,7 @@ function file(p5, fn){ * object. All `File` properties and methods are accessible. * * @for p5.File - * @property file + * @property {File} file * @example *
* @@ -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} array of point objects, each with `x`, `y`, and `alpha` (path angle) properties. * * @example @@ -312,8 +312,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 options: + * @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>} array of point objects, each with `x`, `y`, and `alpha` (path angle) properties. * * @example diff --git a/src/type/textCore.js b/src/type/textCore.js index 2a45168780..cd55559b12 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -499,7 +499,7 @@ function textCore(p5, fn) { * * @method textLeading * @for p5 - * @param {Number} leading The new text leading to apply, in pixels + * @param {Number} [leading] The new text leading to apply, in pixels * @returns {Number} If no arguments are provided, the current text leading * * @example @@ -525,10 +525,6 @@ function textCore(p5, fn) { * * */ - /* - * @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('') + ``; - } 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('') + ``; + } 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,