diff --git a/.gitignore b/.gitignore
index 42f1e447dc..6ddaf0e001 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ docs/reference/*
examples/3d/
.idea
dist/
+*d.ts
p5.zip
bower-repo/
p5-website/
diff --git a/package.json b/package.json
index 978e7d8703..270c442a58 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
"bench:report": "vitest bench --reporter=verbose",
"test": "vitest",
"lint": "eslint .",
- "lint:fix": "eslint --fix ."
+ "lint:fix": "eslint --fix .",
+ "generate-types": "npm run docs && node utils/generate-types && node utils/patch"
},
"lint-staged": {
"Gruntfile.js": "eslint",
diff --git a/src/color/creating_reading.js b/src/color/creating_reading.js
index f848ba3a25..f838c4ceff 100644
--- a/src/color/creating_reading.js
+++ b/src/color/creating_reading.js
@@ -1540,8 +1540,8 @@ function creatingReading(p5, fn){
* colorMode().
*
* @method paletteLerp
- * @param {[p5.Color|String|Number|Number[], Number][]} colors_stops color stops to interpolate from
- * @param {Number} amt number to use to interpolate relative to color stops
+ * @param {Array.>} colors_stops color stops to interpolate from
+ * @param {Number} amt number to use to interpolate relative to color stops
* @return {p5.Color} interpolated color.
*
* @example
diff --git a/utils/convert.js b/utils/convert.js
index a92049298a..dcc478b2a9 100644
--- a/utils/convert.js
+++ b/utils/convert.js
@@ -602,3 +602,5 @@ fs.mkdirSync(path.join(__dirname, '../docs/reference'), { recursive: true });
fs.writeFileSync(path.join(__dirname, '../docs/reference/data.json'), JSON.stringify(converted, null, 2));
fs.writeFileSync(path.join(__dirname, '../docs/reference/data.min.json'), JSON.stringify(converted));
buildParamDocs(JSON.parse(JSON.stringify(converted)));
+
+module.exports= { getAllEntries };
diff --git a/utils/generate-types.js b/utils/generate-types.js
new file mode 100644
index 0000000000..6992f6b41d
--- /dev/null
+++ b/utils/generate-types.js
@@ -0,0 +1,631 @@
+const fs = require('fs');
+const path = require('path');
+const { getAllEntries } = require("./convert");
+
+// Read docs.json
+const data = JSON.parse(fs.readFileSync(path.join(__dirname, '../docs/data.json')));
+
+const organized = {
+ modules: {},
+ classes: {},
+ classitems: [],
+ consts: {}
+ };
+
+// Add this helper function at the top with other helpers
+function normalizeClassName(className) {
+ if (!className || className === 'p5') return 'p5';
+ return className.startsWith('p5.') ? className : `p5.${className}`;
+}
+
+// Organize data into structured format
+function organizeData(data) {
+ const allData = getAllEntries(data);
+
+
+ // Process modules first
+ allData.forEach(entry => {
+ if (entry.tags?.some(tag => tag.title === 'module')) {
+ const { module, submodule } = getModuleInfo(entry);
+ organized.modules[module] = organized.modules[module] || {
+ name: module,
+ submodules: {},
+ classes: {}
+ };
+ if (submodule) {
+ organized.modules[module].submodules[submodule] = true;
+ }
+ }
+ });
+
+ // Process classes
+ allData.forEach(entry => {
+ if (entry.kind === 'class') {
+ const { module, submodule } = getModuleInfo(entry);
+ const className = entry.name;
+ const extendsTag = entry.tags?.find(tag => tag.title === 'extends');
+
+ organized.classes[className] = {
+ name: className,
+ description: extractDescription(entry.description),
+ params: (entry.params || []).map(param => ({
+ name: param.name,
+ type: generateTypeFromTag(param),
+ optional: param.type?.type === 'OptionalType'
+ })),
+ module,
+ submodule,
+ extends: extendsTag?.name || null
+ };
+ }
+ });
+
+ // Process class methods and properties
+ allData.forEach(entry => {
+ if (entry.kind === 'function' || entry.kind === 'property') {
+ const { module, submodule, forEntry } = getModuleInfo(entry);
+
+ // Normalize memberof and forEntry
+ let memberof = entry.memberof;
+ if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) {
+ memberof = 'p5.' + memberof;
+ }
+
+ let normalizedForEntry = forEntry;
+ if (forEntry && forEntry !== 'p5' && !forEntry.startsWith('p5.')) {
+ normalizedForEntry = 'p5.' + forEntry;
+ }
+
+ // Use memberof if available, fallback to forEntry, then default to 'p5'
+ const className = normalizeClassName(memberof || normalizedForEntry || 'p5');
+
+ const isStatic = entry.path?.[0]?.scope === 'static';
+ // Handle overloads
+ 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,
+ overloads
+ });
+ }
+ });
+
+ // Process constants and typedefs
+ allData.forEach(entry => {
+ if (entry.kind === 'constant' || entry.kind === 'typedef') {
+ const { module, submodule, forEntry } = getModuleInfo(entry);
+ organized.consts[entry.name] = {
+ name: entry.name,
+ kind: entry.kind,
+ description: extractDescription(entry.description),
+ type: entry.type ? generateTypeFromTag(entry) : 'any',
+ module,
+ submodule,
+ class: forEntry || 'p5'
+ };
+ }
+ fs.writeFileSync("./consts.json", JSON.stringify(organized.consts, null, 2), 'utf8');
+ });
+
+ return organized;
+}
+
+// Helper function to get module info
+function getModuleInfo(entry) {
+ const moduleTag = entry.tags?.find(tag => tag.title === 'module');
+ const submoduleTag = entry.tags?.find(tag => tag.title === 'submodule');
+ const forTag = entry.tags?.find(tag => tag.title === 'for')
+
+ return {
+ module: moduleTag?.name || 'p5',
+ submodule: submoduleTag?.description,
+ forEntry: forTag?.description || entry.memberof
+ };
+}
+
+// Function to extract text from description object or string
+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') return `\`${child.value}\``;
+ if (child.type === 'code') return `\`${child.value}\``;
+ return '';
+ })
+ .join('')
+ .trim()
+ .replace(/\n{3,}/g, '\n\n');
+ }
+ return '';
+}
+
+// Format comment text for JSDoc
+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');
+}
+
+// Normalize type names to ensure primitive types are lowercase and handle object types
+function normalizeTypeName(type) {
+ if (!type) return 'any';
+
+ // Handle object type notation
+ 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;
+}
+
+// Generate type from tag
+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);
+
+ // Handle array cases
+ if (baseType === 'Array') {
+ const innerType = param.type.applications[0];
+
+ // Handle nested array that represents a tuple
+ if (innerType.type === 'TypeApplication' &&
+ innerType.expression.name === 'Array') {
+ // Get all tuple element types
+ const tupleTypes = innerType.applications
+ .map(app => generateTypeFromTag({ type: app }))
+ .join(', ');
+ return `Array<[${tupleTypes}]>`;
+ }
+
+ // Handle array with union type
+ if (innerType.type === 'UnionType') {
+ const unionTypes = innerType.elements
+ .map(el => generateTypeFromTag({ type: el }))
+ .join(' | ');
+ // If this is part of a tuple structure (has sibling types), wrap in tuple
+ if (param.type.applications.length > 1) {
+ const remainingTypes = param.type.applications
+ .slice(1)
+ .map(app => generateTypeFromTag({ type: app }))
+ .join(', ');
+ return `Array<[${unionTypes}, ${remainingTypes}]>`;
+ }
+ return `Array<${unionTypes}>`;
+ }
+
+ // Regular array
+ const typeParam = generateTypeFromTag({ type: innerType });
+ return `Array<${typeParam}>`;
+ }
+
+ // Regular type application
+ 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':
+ // Check if it's a tuple type (array with specific types for each position)
+ if (param.type.elements) {
+ const tupleTypes = param.type.elements
+ .map(el => generateTypeFromTag({ type: el }))
+ .join(', ');
+ return `[${tupleTypes}]`;
+ }
+ // Regular array type
+ return `${generateTypeFromTag({ type: param.type.elementType })}[]`;
+ default:
+ return 'any';
+ }
+}
+
+// Generate parameter declaration
+function generateParamDeclaration(param) {
+ if (!param) return 'any';
+
+ let type = param.type;
+ const isOptional = param.type?.type === 'OptionalType';
+ if (typeof type === 'string') {
+ type = normalizeTypeName(type);
+ } else if (param.type?.type) {
+ type = generateTypeFromTag(param);
+ } else {
+ type = 'any';
+ }
+
+ return `${param.name}${isOptional ? '?' : ''}: ${type}`;
+}
+
+// Generate function declaration
+function generateFunctionDeclaration(funcDoc) {
+ let output = '';
+
+ // Add Comments
+ 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'; // Add separator between description and tags
+ }
+ funcDoc.tags.forEach(tag => {
+ if (tag.description) {
+ const tagDesc = extractDescription(tag.description);
+ output += formatJSDocComment(`@${tag.title} ${tagDesc}`, 0) + '\n';
+ }
+ });
+ }
+ output += ' */\n';
+ }
+
+ // Generate function signature
+ 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;
+}
+
+// Helper function to generate method declarations
+function generateMethodDeclarations(item, isStatic = false) {
+ let output = '';
+
+ // Add JSDoc comment
+ 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 there are overloads, generate all overload signatures first
+ 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`;
+ });
+ }
+
+ // Generate the implementation signature
+ const params = (item.params || [])
+ .map(param => generateParamDeclaration(param))
+ .join(', ');
+ output += ` ${staticPrefix}${item.name}(${params}): ${item.returnType};\n\n`;
+ } else {
+ // Handle properties
+ const staticPrefix = isStatic ? 'static ' : '';
+ output += ` ${staticPrefix}${item.name}: ${item.returnType};\n\n`;
+ }
+
+ return output;
+}
+
+// Generate class declaration
+function generateClassDeclaration(classDoc, organizedData) {
+ let output = '';
+
+ // Add comments
+ 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';
+ }
+
+ // Get the parent class if it exists
+ const parentClass = classDoc.extends;
+ const extendsClause = parentClass ? ` extends ${parentClass}` : '';
+
+ // Start class declaration with inheritance if applicable
+ const fullClassName = normalizeClassName(classDoc.name);
+ const classDocName = fullClassName.replace('p5.', '');
+ output += `class ${classDocName}${extendsClause} {\n`;
+
+ // Add constructor if there are parameters
+ if (classDoc.params?.length > 0) {
+ output += ' constructor(';
+ output += classDoc.params
+ .map(param => generateParamDeclaration(param))
+ .join(', ');
+ output += ');\n\n';
+ }
+
+ // Get all class items for this class
+ const classItems = organizedData.classitems.filter(item =>
+ item.class === fullClassName ||
+ item.class === fullClassName.replace('p5.', '')
+ );
+
+ // Separate static and instance members
+ const staticItems = classItems.filter(item => item.isStatic);
+ const instanceItems = classItems.filter(item => !item.isStatic);
+
+ // Generate static members
+ staticItems.forEach(item => {
+ output += generateMethodDeclarations(item, true);
+ });
+
+ // Generate instance members
+ instanceItems.forEach(item => {
+ output += generateMethodDeclarations(item, false);
+ });
+
+ output += '}\n\n';
+ return output;
+}
+
+// Generate declaration file for a group of items
+function generateDeclarationFile(items, filePath, organizedData) {
+ let output = '// This file is auto-generated from JSDoc documentation\n\n';
+
+ // Add imports based on dependencies
+ const imports = new Set([`import p5 from 'p5';`]);
+
+ // Check for dependencies
+ 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';
+
+ // Get module name
+ const moduleName = getModuleInfo(items[0]).module;
+
+ // Begin module declaration
+ output += `declare module '${moduleName}' {\n`;
+
+ // Find the class documentation if it exists
+ 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}` : '';
+
+ // Start class declaration
+ output += ` class ${classDocName}${extendsClause} {\n`;
+
+ // Add constructor if there are parameters
+ if (classDoc.params?.length > 0) {
+ output += ' constructor(';
+ output += classDoc.params
+ .map(param => generateParamDeclaration(param))
+ .join(', ');
+ output += ');\n\n';
+ }
+
+ // Get all class items for this class
+ const classItems = organizedData.classitems.filter(item =>
+ item.class === fullClassName ||
+ item.class === fullClassName.replace('p5.', '')
+ );
+
+ // Separate static and instance members
+ const staticItems = classItems.filter(item => item.isStatic);
+ const instanceItems = classItems.filter(item => !item.isStatic);
+ // Generate static members
+ staticItems.forEach(item => {
+ output += generateMethodDeclarations(item, true);
+ });
+ // Generate instance members
+ instanceItems.forEach(item => {
+ output += generateMethodDeclarations(item, false);
+ });
+ output += ' }\n\n';
+ }
+
+ // Add remaining items that aren't part of the class
+ 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;
+ }
+ }
+ });
+
+ // Close module declaration
+ output += '}\n\n';
+
+ // Add default export
+ const exportName = path.basename(filePath, '.js').replace('.', '_');
+ output += `export default function ${exportName}(p5: any, fn: any): void;\n`;
+
+ return output;
+}
+
+// Group items by file
+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;
+}
+
+// Main function to generate all declaration files
+function generateAllDeclarationFiles() {
+ // Organize all data first
+ const organizedData = organizeData(data);
+
+ // Group items by file
+ const fileGroups = groupByFile(getAllEntries(data));
+
+ fileGroups.forEach((items, filePath) => {
+ // Convert the file path to a .d.ts path
+ const parsedPath = path.parse(filePath);
+ const relativePath = path.relative(process.cwd(), filePath);
+ const dtsPath = path.join(
+ path.dirname(relativePath),
+ `${parsedPath.name}.d.ts`
+ );
+
+ // Generate the declaration file content
+ const declarationContent = generateDeclarationFile(items, filePath, organizedData);
+
+ // Create directory if it doesn't exist
+ fs.mkdirSync(path.dirname(dtsPath), { recursive: true });
+
+ // Write the declaration file
+ fs.writeFileSync(dtsPath, declarationContent, 'utf8');
+
+ console.log(`Generated ${dtsPath}`);
+ });
+}
+
+// Run the generator
+generateAllDeclarationFiles();
\ No newline at end of file
diff --git a/utils/patch.js b/utils/patch.js
new file mode 100644
index 0000000000..2231e86361
--- /dev/null
+++ b/utils/patch.js
@@ -0,0 +1,47 @@
+const fs = require('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(
+ "./src/core/structure.d.ts",
+ "function p5(sketch: object, node: string | HTMLElement): void;",
+ "function p5: typeof p5"
+);
+
+replace(
+ "./src/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(
+ "./src/math/random.d.ts",
+ "function random(choices: Array): any;",
+ "function random(choices: T[]): T;"
+);
+
+replace(
+ "./src/utilities/array_functions.d.ts",
+ "function append(array: Array, value: Any): Array;",
+ "function append(array: T[], value: T): T[];"
+);
+
+