├── .gitignore ├── LICENSE ├── README.md ├── analyze.sh ├── build.js ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── webpack.config.js └── webpackDevServer.config.js ├── images ├── contour-integral.png ├── elliptic.png ├── j-invariant.png ├── screenshot-1-original.png ├── screenshot-1.png ├── screenshot-2.png └── zeta.png ├── package-lock.json ├── package.json ├── public ├── 404.html ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── ControlBar │ ├── ControlBar.js │ └── control-bar.css ├── FunctionEditor │ ├── FunctionEditor.js │ └── function-editor.css ├── FunctionPlot │ ├── CoordinateOverlay.js │ ├── function-plot.css │ └── index.js ├── HelpText │ ├── HelpText.js │ └── help-text.css ├── IntegralCalculator │ ├── IntegralPanel.js │ ├── ResultTooltip.js │ ├── index.js │ └── strategies │ │ ├── Circle.js │ │ ├── Freeform.js │ │ ├── FreeformClosed.js │ │ ├── Strategy.js │ │ ├── Test.js │ │ └── util.js ├── OptionsPanel │ └── OptionsPanel.js ├── SidePanel │ ├── SidePanel.js │ └── side-panel.css ├── SliderPanel │ ├── SliderPanel.js │ ├── VariableAdder.js │ ├── VariableSlider.js │ ├── editable-value │ │ ├── EditableValue.css │ │ ├── EditableValue.js │ │ └── parsers.js │ ├── slider-panel.css │ └── slider │ │ ├── Slider.css │ │ └── Slider.js ├── control-panel.css └── util.js ├── favicon.png ├── gl-code ├── README.md ├── complex-functions.js ├── function-list.js ├── grammar.html ├── grammar.js ├── grammar.ne ├── make ├── scene.js ├── scratch │ ├── erf.py │ ├── eta.py │ ├── modular.py │ └── zeta.py ├── shaders.js └── translators │ ├── compiler.js │ ├── custom-functions.js │ ├── derivative.js │ ├── to-glsl.js │ └── to-js.js ├── index.css ├── index.js ├── registerServiceWorker.js └── theme.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | *.swp 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Complex Function Plotter App Icon 2 | A fully-featured, responsive [domain-coloring plotter](https://samuelj.li/complex-function-plotter) for complex-analytic functions. 3 | 4 | ![Screenshot showing domain-coloring plot of a polynomial.](images/screenshot-1.png) 5 | ![Screenshot showing a Jacobi elliptic function.](images/elliptic.png) 6 | ![Screenshot showing a Julia fractal.](images/screenshot-2.png) 7 | ![Screenshot showing the j-invariant.](images/j-invariant.png) 8 | ![Screenshot showing a the built-in contour integrator.](images/contour-integral.png) 9 | ![Screenshot showing the Riemann zeta function.](images/zeta.png) 10 | 11 | ## Features 12 | * Beautiful, fast, interactive domain-coloring plots of complex functions 13 | * Computation of arbitrary contour integrals and residues 14 | * Arbitrary number of auxiliary variables; fast + smooth visualization of their effect on the plotted function 15 | * Arbitrary custom functions via GLSL shader API 16 | * Several toggleable display and graphics options 17 | * Fully anti-aliased and anti-Moiré'd visual output 18 | 19 | ## Documentation 20 | Full documentation is available within the app. 21 | Click the ‘Help’ button in the upper right to open. 22 | -------------------------------------------------------------------------------- /analyze.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | source-map-explorer build/static/js/*.js 3 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = "production" 2 | var BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 3 | 4 | const webpackConfigProd = require("react-scripts/config/webpack.config.prod"); 5 | 6 | webpackConfigProd.plugins.push( 7 | new BundleAnalyzerPlugin({ 8 | analyzerMode: "static", 9 | reportFilename: "report.html", 10 | }) 11 | ) 12 | 13 | require("react-scripts/scripts/build"); 14 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | 12 | if (filename.match(/\.svg$/)) { 13 | return `const React = require('react'); 14 | module.exports = { 15 | __esModule: true, 16 | default: ${assetFilename}, 17 | ReactComponent: React.forwardRef((props, ref) => ({ 18 | $$typeof: Symbol.for('react.element'), 19 | type: 'svg', 20 | ref: ref, 21 | key: null, 22 | props: Object.assign({}, props, { 23 | children: ${assetFilename} 24 | }) 25 | })), 26 | };`; 27 | } 28 | 29 | return `module.exports = ${assetFilename};`; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 9 | Page Redirection 10 | 11 | 12 | If you are not redirected automatically, follow this link. 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgxli/complex-function-plotter/f127a55d73adc34335091bbe254c797ac3d7ce91/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgxli/complex-function-plotter/f127a55d73adc34335091bbe254c797ac3d7ce91/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgxli/complex-function-plotter/f127a55d73adc34335091bbe254c797ac3d7ce91/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 21 | Complex Function Plotter 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Complex Function Plotter", 3 | "name": "Complex Function Plotter", 4 | "icons": [ 5 | { 6 | "src": "./favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "./android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "./android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ], 21 | "start_url": "./index.html", 22 | "display": "standalone" 23 | } 24 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const path = require('path'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const fs = require('fs-extra'); 21 | const webpack = require('webpack'); 22 | const bfj = require('bfj'); 23 | const configFactory = require('../config/webpack.config'); 24 | const paths = require('../config/paths'); 25 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 26 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 27 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 28 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 29 | const printBuildError = require('react-dev-utils/printBuildError'); 30 | 31 | const measureFileSizesBeforeBuild = 32 | FileSizeReporter.measureFileSizesBeforeBuild; 33 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | 36 | // These sizes are pretty large. We'll warn for bundles exceeding them. 37 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 38 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 39 | 40 | const isInteractive = process.stdout.isTTY; 41 | 42 | // Warn and crash if required files are missing 43 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 44 | process.exit(1); 45 | } 46 | 47 | // Process CLI arguments 48 | const argv = process.argv.slice(2); 49 | const writeStatsJson = argv.indexOf('--stats') !== -1; 50 | 51 | // Generate configuration 52 | const config = configFactory('production'); 53 | 54 | // We require that you explicitly set browsers and do not fall back to 55 | // browserslist defaults. 56 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 57 | checkBrowsers(paths.appPath, isInteractive) 58 | .then(() => { 59 | // First, read the current file sizes in build directory. 60 | // This lets us display how much they changed later. 61 | return measureFileSizesBeforeBuild(paths.appBuild); 62 | }) 63 | .then(previousFileSizes => { 64 | // Remove all content but keep the directory so that 65 | // if you're in it, you don't end up in Trash 66 | fs.emptyDirSync(paths.appBuild); 67 | // Merge with the public folder 68 | copyPublicFolder(); 69 | // Start the webpack build 70 | return build(previousFileSizes); 71 | }) 72 | .then( 73 | ({ stats, previousFileSizes, warnings }) => { 74 | if (warnings.length) { 75 | console.log(chalk.yellow('Compiled with warnings.\n')); 76 | console.log(warnings.join('\n\n')); 77 | console.log( 78 | '\nSearch for the ' + 79 | chalk.underline(chalk.yellow('keywords')) + 80 | ' to learn more about each warning.' 81 | ); 82 | console.log( 83 | 'To ignore, add ' + 84 | chalk.cyan('// eslint-disable-next-line') + 85 | ' to the line before.\n' 86 | ); 87 | } else { 88 | console.log(chalk.green('Compiled successfully.\n')); 89 | } 90 | 91 | console.log('File sizes after gzip:\n'); 92 | printFileSizesAfterBuild( 93 | stats, 94 | previousFileSizes, 95 | paths.appBuild, 96 | WARN_AFTER_BUNDLE_GZIP_SIZE, 97 | WARN_AFTER_CHUNK_GZIP_SIZE 98 | ); 99 | console.log(); 100 | 101 | const appPackage = require(paths.appPackageJson); 102 | const publicUrl = paths.publicUrl; 103 | const publicPath = config.output.publicPath; 104 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 105 | printHostingInstructions( 106 | appPackage, 107 | publicUrl, 108 | publicPath, 109 | buildFolder, 110 | useYarn 111 | ); 112 | }, 113 | err => { 114 | console.log(chalk.red('Failed to compile.\n')); 115 | printBuildError(err); 116 | process.exit(1); 117 | } 118 | ) 119 | .catch(err => { 120 | if (err && err.message) { 121 | console.log(err.message); 122 | } 123 | process.exit(1); 124 | }); 125 | 126 | // Create the production build and print the deployment instructions. 127 | function build(previousFileSizes) { 128 | console.log('Creating an optimized production build...'); 129 | 130 | let compiler = webpack(config); 131 | return new Promise((resolve, reject) => { 132 | compiler.run((err, stats) => { 133 | let messages; 134 | if (err) { 135 | if (!err.message) { 136 | return reject(err); 137 | } 138 | messages = formatWebpackMessages({ 139 | errors: [err.message], 140 | warnings: [], 141 | }); 142 | } else { 143 | messages = formatWebpackMessages( 144 | stats.toJson({ all: false, warnings: true, errors: true }) 145 | ); 146 | } 147 | if (messages.errors.length) { 148 | // Only keep the first error. Others are often indicative 149 | // of the same problem, but confuse the reader with noise. 150 | if (messages.errors.length > 1) { 151 | messages.errors.length = 1; 152 | } 153 | return reject(new Error(messages.errors.join('\n\n'))); 154 | } 155 | if ( 156 | process.env.CI && 157 | (typeof process.env.CI !== 'string' || 158 | process.env.CI.toLowerCase() !== 'false') && 159 | messages.warnings.length 160 | ) { 161 | console.log( 162 | chalk.yellow( 163 | '\nTreating warnings as errors because process.env.CI = true.\n' + 164 | 'Most CI servers set it automatically.\n' 165 | ) 166 | ); 167 | return reject(new Error(messages.warnings.join('\n\n'))); 168 | } 169 | 170 | const resolveArgs = { 171 | stats, 172 | previousFileSizes, 173 | warnings: messages.warnings, 174 | }; 175 | if (writeStatsJson) { 176 | return bfj 177 | .write(paths.appBuild + '/bundle-stats.json', stats.toJson()) 178 | .then(() => resolve(resolveArgs)) 179 | .catch(error => reject(new Error(error))); 180 | } 181 | 182 | return resolve(resolveArgs); 183 | }); 184 | }); 185 | } 186 | 187 | function copyPublicFolder() { 188 | fs.copySync(paths.appPublic, paths.appBuild, { 189 | dereference: true, 190 | filter: file => file !== paths.appHtml, 191 | }); 192 | } 193 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const fs = require('fs'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const paths = require('../config/paths'); 32 | const configFactory = require('../config/webpack.config'); 33 | const createDevServerConfig = require('../config/webpackDevServer.config'); 34 | 35 | const useYarn = fs.existsSync(paths.yarnLockFile); 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 45 | const HOST = process.env.HOST || '0.0.0.0'; 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ); 55 | console.log( 56 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 57 | ); 58 | console.log( 59 | `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}` 60 | ); 61 | console.log(); 62 | } 63 | 64 | // We require that you explictly set browsers and do not fall back to 65 | // browserslist defaults. 66 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 67 | checkBrowsers(paths.appPath, isInteractive) 68 | .then(() => { 69 | // We attempt to use the default port but if it is busy, we offer the user to 70 | // run on a different port. `choosePort()` Promise resolves to the next free port. 71 | return choosePort(HOST, DEFAULT_PORT); 72 | }) 73 | .then(port => { 74 | if (port == null) { 75 | // We have not found a port. 76 | return; 77 | } 78 | const config = configFactory('development'); 79 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 80 | const appName = require(paths.appPackageJson).name; 81 | const useTypeScript = fs.existsSync(paths.appTsConfig); 82 | const urls = prepareUrls(protocol, HOST, port); 83 | const devSocket = { 84 | warnings: warnings => 85 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 86 | errors: errors => 87 | devServer.sockWrite(devServer.sockets, 'errors', errors), 88 | }; 89 | // Create a webpack compiler that is configured with custom messages. 90 | const compiler = createCompiler({ 91 | appName, 92 | config, 93 | devSocket, 94 | urls, 95 | useYarn, 96 | useTypeScript, 97 | webpack, 98 | }); 99 | // Load proxy config 100 | const proxySetting = require(paths.appPackageJson).proxy; 101 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 102 | // Serve webpack assets generated by the compiler over a web server. 103 | const serverConfig = createDevServerConfig( 104 | proxyConfig, 105 | urls.lanUrlForConfig 106 | ); 107 | const devServer = new WebpackDevServer(compiler, serverConfig); 108 | // Launch WebpackDevServer. 109 | devServer.listen(port, HOST, err => { 110 | if (err) { 111 | return console.log(err); 112 | } 113 | if (isInteractive) { 114 | clearConsole(); 115 | } 116 | console.log(chalk.cyan('Starting the development server...\n')); 117 | openBrowser(urls.localUrlForBrowser); 118 | }); 119 | 120 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 121 | process.on(sig, function() { 122 | devServer.close(); 123 | process.exit(); 124 | }); 125 | }); 126 | }) 127 | .catch(err => { 128 | if (err && err.message) { 129 | console.log(err.message); 130 | } 131 | process.exit(1); 132 | }); 133 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI, in coverage mode, explicitly adding `--no-watch`, 42 | // or explicitly running all tests 43 | if ( 44 | !process.env.CI && 45 | argv.indexOf('--coverage') === -1 && 46 | argv.indexOf('--no-watch') === -1 && 47 | argv.indexOf('--watchAll') === -1 48 | ) { 49 | // https://github.com/facebook/create-react-app/issues/5210 50 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 51 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 52 | } 53 | 54 | // Jest doesn't have this option so we'll remove it 55 | if (argv.indexOf('--no-watch') !== -1) { 56 | argv = argv.filter(arg => arg !== '--no-watch'); 57 | } 58 | 59 | 60 | jest.run(argv); 61 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | overscroll-behavior: none; 7 | } 8 | 9 | #app { 10 | position: absolute; 11 | display: flex; 12 | flex-direction: column; 13 | 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | right: 0; 18 | } 19 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import 'katex/dist/katex.min.css'; 4 | 5 | import {ThemeProvider as MuiThemeProvider} from '@material-ui/core/styles'; 6 | import theme from './theme.js'; 7 | 8 | import ControlBar from './components/ControlBar/ControlBar.js'; 9 | import FunctionPlot from './components/FunctionPlot'; 10 | 11 | import SidePanel from './components/SidePanel/SidePanel.js'; 12 | import './components/control-panel.css'; 13 | 14 | import SliderPanel from './components/SliderPanel/SliderPanel.js'; 15 | import OptionsPanel from './components/OptionsPanel/OptionsPanel.js'; 16 | 17 | import IntegralCalculator from './components/IntegralCalculator'; 18 | import IntegralPanel from './components/IntegralCalculator/IntegralPanel'; 19 | 20 | import FunctionEditor from './components/FunctionEditor/FunctionEditor.js'; 21 | 22 | import HelpText from './components/HelpText/HelpText.js'; 23 | 24 | import {parseExpression} from './gl-code/complex-functions'; 25 | 26 | 27 | const defaultShader = `vec2 mapping(vec2 z) { 28 | z += t; 29 | vec2 c = z; 30 | for (int i=0; i<64; i++) { 31 | z = cmul(z, z) + c; 32 | } 33 | return z; 34 | }`; 35 | 36 | 37 | function extractVariables(expression) { 38 | if (!Array.isArray(expression)) {return new Set();} 39 | if (expression[0] === 'variable' && expression[1] !== 'z') { 40 | return new Set([expression[1]]); 41 | } 42 | 43 | let output = new Set(); 44 | for (let entry of expression) { 45 | output = new Set([...output, ...extractVariables(entry)]); 46 | } 47 | return output; 48 | } 49 | 50 | 51 | class App extends React.Component { 52 | state = { 53 | expressionText: 'z-t', 54 | expression: ['sub', ['variable', 'z'], ['variable', 't']], 55 | expressionError: false, 56 | typingTimer: null, 57 | 58 | customShader: defaultShader, 59 | shaderError: false, 60 | 61 | menuOpen: false, 62 | helpOpen: false, 63 | variableChanging: false, 64 | 65 | integrationStrategy: null, 66 | 67 | variables: { 68 | log_scale: 5, 69 | center_x: 0, 70 | center_y: 0, 71 | 72 | t: 0.3, 73 | 74 | enable_checkerboard: 1, 75 | invert_gradient: 0, 76 | continuous_gradient: 0, 77 | enable_axes: 1, 78 | 79 | custom_function: 0, 80 | } 81 | } 82 | 83 | componentDidMount() { 84 | window.addEventListener('hashchange', this.handleHashChange.bind(this)); 85 | 86 | document.addEventListener( 87 | 'gesturestart', 88 | (event) => this.preventDefault(event), 89 | {passive: false} 90 | ); 91 | document.addEventListener( 92 | 'touchmove', 93 | (event) => this.preventDefault(event), 94 | {passive: false} 95 | ); 96 | 97 | this.handleHashChange(); 98 | } 99 | 100 | preventDefault(event) { 101 | const tagName = event.target.tagName.toLowerCase(); 102 | if (tagName === 'canvas') { 103 | event.preventDefault(); 104 | } else { 105 | return event; 106 | } 107 | } 108 | 109 | // Toolbar buttons 110 | handleMenuButton() { 111 | this.setState({menuOpen: !this.state.menuOpen}); 112 | } 113 | 114 | handleHelpButton() { 115 | this.setState({helpOpen: !this.state.helpOpen}); 116 | } 117 | 118 | // Variable Sliders 119 | handleVariableUpdate(update) { 120 | this.refs.plot.handleVariableUpdate(update); 121 | } 122 | 123 | handleVariableAdd(name, value) { 124 | const variables = this.state.variables; 125 | variables[name] = value; 126 | this.setState({variables: variables}); 127 | } 128 | 129 | handleVariableRemove(name) { 130 | const variables = this.state.variables; 131 | delete variables[name]; 132 | this.setState({variables: variables}); 133 | } 134 | 135 | handleOptionToggle(name) { 136 | const variables = this.state.variables; 137 | variables[name] = (variables[name] < 0.5) ? 1 : 0; 138 | this.handleVariableUpdate({[name]: variables[name]}); 139 | this.setState({variables}); 140 | } 141 | 142 | setErrorMessage(message) { 143 | const {variables, expressionError, shaderError} = this.state; 144 | 145 | if (variables.custom_function > 0.5) { 146 | if (message !== shaderError) { 147 | this.setState({shaderError: message}); 148 | } 149 | } else { 150 | if (message !== expressionError) { 151 | this.setState({expressionError: message}); 152 | } 153 | } 154 | } 155 | 156 | handleHashChange() { 157 | const hash = window.location.hash; 158 | if (hash === '') { 159 | this.setExpression('z-t', true); 160 | } else { 161 | this.setExpression(decodeURIComponent(hash.slice(1)), true); 162 | } 163 | } 164 | 165 | setExpression(text, fromHash) { 166 | text = text.toLowerCase(); 167 | 168 | window.history.replaceState( 169 | undefined, undefined, 170 | '#' + encodeURIComponent(text) 171 | ); 172 | 173 | this.setState({ 174 | expressionText: text, 175 | integrationStrategy: null, 176 | }); 177 | 178 | if (fromHash) { 179 | const expression = parseExpression(text.trim()); 180 | const variables = extractVariables(expression); 181 | const newVariables = {...this.state.variables}; 182 | 183 | for (let entry of variables) { 184 | newVariables[entry] = 0.3; 185 | } 186 | 187 | this.setState({expression, variables: newVariables}); 188 | } else { 189 | clearTimeout(this.typingTimer); 190 | this.typingTimer = setTimeout(this.finalizeExpression.bind(this), 200); 191 | } 192 | } 193 | 194 | finalizeExpression() { 195 | const {expressionText} = this.state; 196 | const expression = parseExpression(expressionText.trim()); 197 | this.setState({expression}); 198 | } 199 | 200 | setCustomShader(text) { 201 | this.setState({customShader: text}); 202 | } 203 | 204 | renderFunctionEditor() { 205 | const {variables, customShader, shaderError} = this.state; 206 | if (variables.custom_function > 0.5) { 207 | return ( 208 | 213 | ); 214 | } else { 215 | return; 216 | } 217 | } 218 | 219 | render() { 220 | const { 221 | variables, 222 | customShader, 223 | expression, expressionText, 224 | menuOpen, helpOpen, 225 | expressionError, 226 | integrationStrategy, 227 | variableChanging, 228 | } = this.state; 229 | 230 | const useCustomShader = variables.custom_function > 0.5; 231 | 232 | const ast = useCustomShader ? customShader : expression; 233 | 234 | return ( 235 | 236 |
237 | 248 | 255 | this.setState({variableChanging: x})} 261 | /> 262 | this.setState({helpOpen: false, menuOpen: false})} 265 | openCalculator={integrationStrategy => this.setState({integrationStrategy})} 266 | /> 267 | 278 | 286 | {this.renderFunctionEditor()} 287 | 288 | this.setState({integrationStrategy: null})} 293 | /> 294 | 300 | 307 | { 309 | this.setExpression(expression); 310 | this.handleVariableUpdate({ 311 | ...this.state.variables, 312 | custom_function: 0, 313 | }); 314 | }} 315 | closeMenu={() => this.setState({helpOpen: false})} 316 | /> 317 | 318 |
319 |
320 | ); 321 | } 322 | } 323 | 324 | export default App; 325 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/ControlBar/ControlBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import Toolbar from '@material-ui/core/Toolbar'; 5 | import TextField from '@material-ui/core/TextField'; 6 | import Button from '@material-ui/core/Button'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | 9 | import MenuIcon from '@material-ui/icons/Menu'; 10 | 11 | import './control-bar.css'; 12 | 13 | 14 | class ControlPanel extends React.PureComponent { 15 | handleTextChange(event) { 16 | this.props.onChange(event.target.value); 17 | } 18 | 19 | render() { 20 | const {disabled, onMenuButton, onHelpButton, error, value} = this.props; 21 | return ( 22 | 23 | 24 | 27 | 28 | 29 |
30 | 480 ? 'Enter a complex function of z' : 'f(z)'} 33 | value={value} 34 | onChange={(event) => this.handleTextChange(event)} 35 | error={error} 36 | autoFocus 37 | fullWidth 38 | disabled={disabled} 39 | /> 40 |
41 |
42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | export default ControlPanel; 51 | -------------------------------------------------------------------------------- /src/components/ControlBar/control-bar.css: -------------------------------------------------------------------------------- 1 | #app-bar { 2 | display: inline-block; 3 | padding: 0 8px; 4 | 5 | z-index: 1300; 6 | background-color: #FFF; 7 | } 8 | 9 | #function-input { 10 | margin-left: 3px; 11 | margin-right: 75px; 12 | 13 | overflow: hidden; 14 | flex-grow: 1; 15 | 16 | font-size: 18px; 17 | font-family: 'KaTeX_Main'; 18 | } 19 | 20 | #help-button { 21 | position: absolute; 22 | right: 0; 23 | } 24 | 25 | @media (max-width: 480px) { 26 | #function-input { 27 | font-size: 16px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/FunctionEditor/FunctionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Typography from '@material-ui/core/Typography'; 4 | 5 | import Editor from 'react-simple-code-editor'; 6 | import { highlight, languages } from 'prismjs/components/prism-core'; 7 | import 'prismjs/components/prism-clike'; 8 | import 'prismjs/components/prism-c'; 9 | import 'prismjs/components/prism-glsl'; 10 | import 'prismjs/themes/prism-okaidia.css'; 11 | 12 | import './function-editor.css'; 13 | 14 | export default ({value, onChange, errorMessage}) =>
15 | 16 | Please read the “Advanced Features” documentation in the right pane before proceeding! 17 |
18 | Compilation errors are currently logged to the developer console. 19 |
20 | {var a = highlight(code, languages.glsl); console.log(a); return a;}} 24 | padding={10} 25 | style={{ 26 | fontFamily: '"Fira code", "Fira Mono", monospace', 27 | fontSize: 14, 28 | backgroundColor: 'hsl(200, 20%, 10%)', 29 | color: 'white', 30 | marginTop: 10, 31 | borderRadius: 8, 32 | }} 33 | /> 34 | 35 |
36 | This pane is resizable! Use the handle on the bottom-right corner. 37 |
38 |
; 39 | -------------------------------------------------------------------------------- /src/components/FunctionEditor/function-editor.css: -------------------------------------------------------------------------------- 1 | #function-editor { 2 | margin: 20px; 3 | margin-top: 0; 4 | } 5 | 6 | @media (max-width: 480px) { 7 | #function-editor .resize-helper { 8 | display: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/FunctionPlot/CoordinateOverlay.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | 3 | import {InlineMath} from 'react-katex'; 4 | 5 | import {formatComplex} from '../util'; 6 | 7 | 8 | class CoordinateOverlay extends PureComponent { 9 | state = { 10 | faded: false 11 | } 12 | 13 | constructor(props) { 14 | super(props); 15 | 16 | this.fadeTimer = null; 17 | this.lastX = null; 18 | this.lastY = null; 19 | } 20 | 21 | hasChanged() { 22 | const {x, y} = this.props; 23 | const changed = (this.lastX !== x) || (this.lastY !== y); 24 | 25 | this.lastX = x; 26 | this.lastY = y; 27 | 28 | return changed; 29 | } 30 | 31 | componentDidMount() { 32 | this.componentDidUpdate(); 33 | } 34 | 35 | componentDidUpdate() { 36 | if (this.hasChanged()) { 37 | if (this.fadeTimer !== null) { 38 | clearTimeout(this.fadeTimer); 39 | } 40 | this.fadeTimer = setTimeout(() => this.setState({faded: true}), 2e3); 41 | this.setState({faded: false}); 42 | } 43 | } 44 | 45 | render() { 46 | const {faded} = this.state; 47 | const {x, y, mapping} = this.props; 48 | 49 | const [u, v] = mapping([x, y]); 50 | 51 | return
52 | {'z = ' + formatComplex(x, y, 3)} 53 | {isFinite(u) && isFinite(v) ? {'f(z) = ' + formatComplex(u, v, 3)} : null} 54 | 93 |
; 94 | } 95 | } 96 | 97 | export default CoordinateOverlay; 98 | -------------------------------------------------------------------------------- /src/components/FunctionPlot/function-plot.css: -------------------------------------------------------------------------------- 1 | #function-plot { 2 | position: absolute; 3 | 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | 9 | background: #222; 10 | overflow: hidden; 11 | } 12 | 13 | #function-plot > canvas { 14 | position: absolute; 15 | 16 | width: 100%; 17 | height: 100%; 18 | 19 | z-index: 10; 20 | } 21 | 22 | #function-plot > canvas.axes { 23 | z-index: 11; 24 | pointer-events: none; 25 | user-select: none; 26 | } 27 | 28 | #fallback-text { 29 | position: absolute; 30 | top: 0; 31 | bottom: 0; 32 | left: 0; 33 | right: 0; 34 | 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | justify-content: center; 39 | 40 | text-align: center; 41 | font-size: 40px; 42 | 43 | background-color: hsl(200, 15%, 10%); 44 | color: white; 45 | 46 | font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; 47 | 48 | z-index: 1; 49 | } 50 | 51 | #fallback-text .small { 52 | font-size: 16px; 53 | margin: 0 30px; 54 | margin-top: 15px; 55 | } 56 | -------------------------------------------------------------------------------- /src/components/FunctionPlot/index.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | 3 | import {initializeScene, drawScene} from '../../gl-code/scene.js'; 4 | import toJS from '../../gl-code/translators/to-js'; 5 | 6 | import CoordinateOverlay from './CoordinateOverlay'; 7 | 8 | import './function-plot.css'; 9 | 10 | 11 | const FPS_LIMIT = 70; 12 | 13 | let canvasSize = []; 14 | 15 | function pixelToPlot(x, y, variables) { 16 | const scale = Math.exp(variables.log_scale); 17 | const offset_x = variables.center_x; 18 | const offset_y = variables.center_y; 19 | 20 | const plot_x = (x - canvasSize[0]/2) / scale + offset_x; 21 | const plot_y = (canvasSize[1]/2 - y) / scale + offset_y; 22 | return [plot_x, plot_y]; 23 | } 24 | 25 | function plotToPixel(plot_x, plot_y, variables) { 26 | const scale = Math.exp(variables.log_scale); 27 | const offset_x = variables.center_x; 28 | const offset_y = variables.center_y; 29 | 30 | const x = scale * (plot_x - offset_x) + canvasSize[0]/2; 31 | const y = -scale * (plot_y - offset_y) - canvasSize[1]/2; 32 | return [x, y]; 33 | } 34 | 35 | class FunctionPlot extends PureComponent { 36 | state = { 37 | position: [NaN, NaN, 0], 38 | mouseDown: false, 39 | } 40 | 41 | constructor(props) { 42 | super(props); 43 | 44 | this.gl = null; 45 | 46 | this.variableLocations = {}; 47 | this.initialized = false; 48 | 49 | this.lastUpdate = null; // Timestamp of last update, for debouncing 50 | } 51 | 52 | componentDidMount() { 53 | window.addEventListener('resize', this.compilePlot.bind(this)); 54 | 55 | this.initializeWebGL(); 56 | this.componentDidUpdate(); 57 | this.compilePlot(); 58 | } 59 | 60 | // Perform a full update of the plot (expensive!) 61 | compilePlot() { 62 | const {onError} = this.props; 63 | this.initializePlot(); 64 | this.updatePlot(); 65 | onError(!this.initialized); 66 | } 67 | 68 | componentDidUpdate() { 69 | const {expression, variables} = this.props; 70 | const keys = Object.keys(variables).toString(); 71 | if (this.lastExpression !== expression || keys !== this.lastKeys) { 72 | this.compilePlot(); 73 | } 74 | this.lastExpression = expression; 75 | this.lastKeys = keys; 76 | 77 | } 78 | 79 | getPosition(mouseEvent) { 80 | const nativeEvent = mouseEvent.nativeEvent; 81 | 82 | if (nativeEvent.targetTouches !== undefined) { 83 | const touches = nativeEvent.targetTouches; 84 | if (touches.length === 1) { 85 | return [touches[0].pageX, touches[0].pageY, 1]; 86 | } else { 87 | const [x0, y0] = [touches[0].pageX, touches[0].pageY]; 88 | const [x1, y1] = [touches[1].pageX, touches[1].pageY]; 89 | 90 | const centerX = (x0 + x1) / 2; 91 | const centerY = (y0 + y1) / 2; 92 | const logDistance = Math.log(Math.hypot(x1-x0, y1-y0)); 93 | return [centerX, centerY, logDistance]; 94 | } 95 | } else { 96 | return [nativeEvent.pageX, nativeEvent.pageY, 1]; 97 | } 98 | } 99 | 100 | updatePosition(mouseEvent) { 101 | this.setState({position: this.getPosition(mouseEvent)}); 102 | } 103 | 104 | handleZoom(wheelEvent, deltaLogScale) { 105 | const {position} = this.state; 106 | const {variables} = this.props; 107 | 108 | if (!position.every(isFinite)) {return;} 109 | const [x, y, _] = position; 110 | 111 | // Recenter plot at scroll location 112 | const [mouse_plotx, mouse_ploty] = pixelToPlot(x, y, variables); 113 | variables.center_x = mouse_plotx; 114 | variables.center_y = mouse_ploty; 115 | 116 | // Change scale 117 | if (deltaLogScale === undefined) { 118 | deltaLogScale = (wheelEvent.deltaY > 0) ? -0.05: 0.05; 119 | } 120 | variables.log_scale += deltaLogScale; 121 | 122 | // Move center back onto mouse 123 | const [new_mouse_plotx, new_mouse_ploty] = pixelToPlot(x, y, variables); 124 | const [x_shift, y_shift] = [new_mouse_plotx - mouse_plotx, new_mouse_ploty - mouse_ploty]; 125 | variables.center_x -= x_shift; 126 | variables.center_y -= y_shift; 127 | 128 | this.updatePlot(); 129 | } 130 | 131 | handleMouseDown(event) { 132 | this.updatePosition(event); 133 | this.setState({mouseDown: true}); 134 | } 135 | 136 | handleMouseUp(event) { 137 | this.setState({mouseDown: false}); 138 | } 139 | 140 | handleMouseMove(event) { 141 | const {mouseDown} = this.state; 142 | const {variables} = this.props; 143 | 144 | // Handle dragging of plot 145 | if (mouseDown) { 146 | const {position} = this.state; 147 | const [x, y, logDistance] = this.getPosition(event); 148 | 149 | const deltaPosition = [ 150 | x - position[0], 151 | y - position[1], 152 | logDistance - position[2] 153 | ]; 154 | 155 | const scale = Math.exp(variables.log_scale); 156 | variables.center_x -= deltaPosition[0] / scale; 157 | variables.center_y += deltaPosition[1] / scale; 158 | this.handleZoom(null, deltaPosition[2]); 159 | this.updatePlot(); 160 | } 161 | this.updatePosition(event); 162 | } 163 | 164 | handleTouchStart(event) { 165 | this.handleMouseDown(event); 166 | } 167 | 168 | handleTouchMove(event) { 169 | this.handleMouseMove(event); 170 | } 171 | 172 | handleTouchEnd(event) { 173 | this.handleMouseUp(event); 174 | } 175 | 176 | handleVariableUpdate(update) { 177 | const {variables} = this.props; 178 | 179 | for (const [name, value] of Object.entries(update)) { 180 | variables[name] = value; 181 | } 182 | 183 | this.updatePlot(); 184 | } 185 | 186 | initializeWebGL() { 187 | const canvas = this.refs.canvas; 188 | const axes = this.refs.axes; 189 | this.gl = canvas.getContext('webgl'); 190 | this.axes = axes.getContext('2d'); 191 | 192 | if (this.gl === null) { 193 | console.error('Unable to initialize WebGL.'); 194 | return null; 195 | } 196 | } 197 | 198 | initializePlot() { 199 | const {expression, variables} = this.props; 200 | 201 | const canvas = this.refs.canvas; 202 | const axes = this.refs.axes; 203 | let dpr = window.devicePixelRatio; 204 | 205 | // Resize canvas and WebGL viewport 206 | canvas.width = canvas.offsetWidth * dpr; 207 | canvas.height = canvas.offsetHeight * dpr; 208 | 209 | axes.width = axes.offsetWidth * dpr; 210 | axes.height = axes.offsetHeight * dpr; 211 | 212 | // Record canvas size and update GL viewport 213 | canvasSize = [canvas.offsetWidth, canvas.offsetHeight]; 214 | this.gl.viewport(0, 0, canvas.width, canvas.height); 215 | 216 | // Initialize scene and obtain WebGL variable pointers 217 | const variableLocations = initializeScene( 218 | this.gl, 219 | expression, 220 | variables.custom_function > 0.5, 221 | Object.keys(variables) 222 | ); 223 | 224 | // Check if initialized 225 | this.initialized = (variableLocations !== null); 226 | if (this.initialized) { 227 | this.variableLocations = variableLocations; 228 | } 229 | } 230 | 231 | updatePlot() { 232 | const {variables} = this.props; 233 | const variableAssignments = {}; 234 | for (const [name, value] of Object.entries(variables)) { 235 | variableAssignments[name] = [this.variableLocations[name], value]; 236 | } 237 | 238 | // Debounce 239 | const now = +new Date(); 240 | if (this.lastUpdate === null || now - this.lastUpdate > 1e3 / FPS_LIMIT) { 241 | drawScene(this.gl, variableAssignments, this.axes); 242 | this.lastUpdate = now; 243 | } 244 | } 245 | 246 | render() { 247 | const {position, mouseDown} = this.state; 248 | const {expression, variables} = this.props; 249 | 250 | const [x, y] = pixelToPlot(position[0], position[1], variables); 251 | const mapping = toJS(expression, variables); 252 | 253 | return ( 254 |
255 |
256 | Loading... 257 |
If nothing happens, please check that Javascript and WebGL are enabled.
258 |
259 | this.handleMouseDown(event)} 263 | onMouseMove={(event) => this.handleMouseMove(event)} 264 | onMouseUp={(event) => this.handleMouseUp(event)} 265 | 266 | onMouseEnter={(event) => this.handleMouseMove(event)} 267 | onMouseLeave={(event) => this.handleMouseUp(event)} 268 | 269 | onTouchStart={(event) => this.handleTouchStart(event)} 270 | onTouchMove={(event) => this.handleTouchMove(event)} 271 | onTouchEnd={(event) => this.handleTouchEnd(event)} 272 | 273 | onWheel={(event) => this.handleZoom(event)} 274 | 275 | className='full-canvas main-plot' 276 | /> 277 | 281 | 282 | {isFinite(x) && isFinite(y) ? 283 | 284 | : null} 285 | 286 | 291 |
292 | ); 293 | } 294 | } 295 | 296 | export {pixelToPlot, plotToPixel}; 297 | export default FunctionPlot; 298 | -------------------------------------------------------------------------------- /src/components/HelpText/HelpText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | 5 | import Typography from '@material-ui/core/Typography'; 6 | import Chip from '@material-ui/core/Chip'; 7 | import Accordion from '@material-ui/core/Accordion'; 8 | import AccordionSummary from '@material-ui/core/AccordionSummary'; 9 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 10 | 11 | import MenuIcon from '@material-ui/icons/Menu'; 12 | import DeleteIcon from '@material-ui/icons/Delete'; 13 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 14 | 15 | import {Light as SyntaxHighlighter} from 'react-syntax-highlighter'; 16 | import glslHighlighter from 'react-syntax-highlighter/dist/esm/languages/hljs/glsl'; 17 | import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 18 | 19 | import './help-text.css'; 20 | 21 | SyntaxHighlighter.registerLanguage('glsl', glslHighlighter); 22 | 23 | 24 | const styles = theme => ({ 25 | buttonStyle: { 26 | ...theme.typography.button 27 | }, 28 | chip: { 29 | margin: 3 30 | } 31 | }); 32 | 33 | const supportedFunctions = [ 34 | { 35 | name: 'Operators', 36 | entries: ['+', '-', '*', '/', '^', '!', '%'] 37 | }, 38 | { 39 | name: 'Trigonometry', 40 | entries: [ 41 | 'sin', 'cos', 'tan', 'sec', 'csc', 'cot', 42 | 'arcsin', 'arccos', 'arctan', 'arcsec', 'arccsc', 'arccot', 43 | 'sinh', 'cosh', 'tanh', 'sech', 'csch', 'coth', 44 | 'arsinh', 'arcosh', 'artanh', 'arsech', 'arcsch', 'arcoth' 45 | ] 46 | }, 47 | { 48 | name: 'Exponential', 49 | entries: ['exp', 'log', 'ln'] 50 | }, 51 | { 52 | name: 'Constants', 53 | entries: ['e', 'pi', 'phi', 'tau'], 54 | }, 55 | { 56 | name: 'Elliptic/Modular', 57 | entries: [ 58 | 'nome', 59 | 'sn(z, k)', 60 | 'cn(z, k)', 61 | 'dn(z, k)', 62 | 'wp(z, τ)', 63 | "wp'(z, τ)", 64 | 'sm', 'cm', 65 | 'theta00(z, τ)', 66 | 'theta01(z, τ)', 67 | 'theta10(z, τ)', 68 | 'theta11(z, τ)', 69 | 'j(τ)', 70 | 'E4(τ)', 71 | 'E6(τ)', 72 | 'E8(τ)', 73 | 'E10(τ)', 74 | 'E12(τ)', 75 | 'E14(τ)', 76 | 'E16(τ)', 77 | ] 78 | }, 79 | { 80 | name: 'Special Functions', 81 | entries: [ 82 | 'sqrt', 83 | 'gamma', 'beta', 'binomial', 'eta', 'zeta', 84 | 'erf', 85 | 'lambertw', 86 | ] 87 | }, 88 | { 89 | name: 'Miscellaneous', 90 | entries: [ 91 | 'abs', 'arg', 'sgn', 'cis', 92 | 'conj', 93 | 'real', 'imag', 94 | 'floor', 'ceil', 'round', 'step', 95 | ] 96 | }, 97 | { 98 | name: 'Higher-Order Functions', 99 | entries: [ 100 | 'sum', 'product', 'derivative' 101 | ] 102 | } 103 | ]; 104 | 105 | 106 | const EXAMPLES = { 107 | '+': 'z + t', 108 | '-': 'z - t', 109 | '*': 't * z', 110 | '/': '1/z', 111 | '^': 'z^z^z^z^z^z^z', 112 | '!': 'z!', 113 | '%': 'z % (1+t*i)', 114 | 115 | 'e': null, 116 | 'pi': null, 117 | 'tau': null, 118 | 'phi': null, 119 | 120 | 'beta': 'beta(z, 3)', 121 | 'binomial': 'binom(z, 5)', 122 | 123 | 'nome': 'j(nome(z))', 124 | 'sn(z, k)': 'sn(z, t + i)', 125 | 'cn(z, k)': 'cn(z, t + i)', 126 | 'dn(z, k)': 'dn(z, t + i)', 127 | 'wp(z, τ)': 'wp(z, t + i)', 128 | "wp'(z, τ)": "wp'(z, t + i)", 129 | 'theta00(z, τ)': 'theta00(z, t + i)', 130 | 'theta01(z, τ)': 'theta01(z, t + i)', 131 | 'theta10(z, τ)': 'theta10(z, t + i)', 132 | 'theta11(z, τ)': 'theta11(z, t + i)', 133 | 'j(τ)': 'j(z)', 134 | 'E4(τ)': 'e4(z)', 135 | 'E6(τ)': 'e6(z)', 136 | 'E8(τ)': 'e8(z)', 137 | 'E10(τ)': 'e10(z)', 138 | 'E12(τ)': 'e12(z)', 139 | 'E14(τ)': 'e14(z)', 140 | 'E16(τ)': 'e16(z)', 141 | 142 | 'sum': 'sum(z^n/n!, n, 0, 5)', 143 | 'product': 'prod(1-z/n, n, 1, 5)', 144 | 'derivative': 'diff(cos(z*t), t)', 145 | }; 146 | 147 | 148 | class HelpText extends React.Component { 149 | constructor(props) { 150 | super(props); 151 | this.classes = props.classes; 152 | } 153 | 154 | shouldComponentUpdate() { 155 | return false; 156 | } 157 | 158 | renderChips(entries) { 159 | const {setExpression, closeMenu} = this.props; 160 | const showExample = (label) => setExpression( 161 | EXAMPLES[label] || `${label}(z)` 162 | ); 163 | return entries.map(label => 164 | { 168 | if (window.innerWidth <= 900) {closeMenu();} 169 | showExample(label); 170 | }} 171 | className={this.classes.chip} 172 | /> 173 | ); 174 | } 175 | 176 | renderSupportedFunctions() { 177 | const sections = [] 178 | for (const section of supportedFunctions) { 179 | sections.push( 180 |
181 |

{section.name}

182 | {this.renderChips(section.entries)} 183 |
184 | ); 185 | } 186 | return sections; 187 | } 188 | 189 | render() { 190 | return ( 191 |
192 |

193 | logo 194 |
195 | Complex Function Plotter 196 |
197 | Made with {'<3'} by Samuel J. Li 198 |
199 |
200 |

201 | 202 |

Introduction

203 |

Creates interactive domain-coloring plots of complex functions.

204 |

Each point on the complex plane is colored according to the value of the function at that point. 205 | Hue and brightness are used to display phase and magnitude, respectively.

206 |

To increase the displayable range of values, brightness cycles from dark to light repeatedly with magnitude, jumping at every power of two. 207 | The “edges” of regions with different brightness correspond to curves along which the function has constant magnitude. 208 |

209 |

This method allows for quick visualization of the locations and orders of any poles and zeros. 210 | For more information, see the Wikipedia article on domain coloring.

211 | 212 |

Controls

213 |

To plot a complex function, enter its expression in the top bar. Parentheses (curved or square), complex number literals, and arbitrary variables (see “Variables” below) are supported. Input is case-insensitive.

214 |

A red underline will appear if the expression cannot be plotted. You might have made a syntax error or forgot to declare a variable.

215 |

Drag the plot to pan, and use the scroll wheel to zoom in and out.

216 | 217 |

Supported Functions

218 |

Click on any function to see an example.

219 |
220 | Don’t see what you want? Request a function 221 | {this.renderSupportedFunctions()} 222 |

223 | Warning: Large sums or products may cause the plotter to freeze! 224 |
225 | 226 |

Variables

227 |

Expressions can contain arbitrary user-defined variables.

228 |

229 | Click on the icon in the upper-left corner to open the variables panel. 230 | Variables can be added with the Add button and removed with the button.

231 | 232 |

233 | Click on the variable’s current value or the slider’s upper & lower bounds to edit them. 234 |

235 | 236 |

Any string of lowercase letters can be used as a variable name, except for reserved functions, the built-in variable z, and the constant i.

237 | 238 |

Contour Integrals

239 |

The calculator is able to compute arbitrary contour integrals and residues. The following options 240 | are available under the ‘Contour Integral’ section of the left pane:

241 | 242 |
243 |

Freeform

244 |

After selecting this option, click and drag to draw an arbitrary contour. The integral of the currently plotted function along the given contour will be displayed.

245 | 246 |

Freeform (Closed Loop)

247 |

Same as ‘Freeform,’ except that the contour is closed with a line segment connecting the start and end points after it is drawn.

248 | 249 |

Circular Contour

250 |

After selecting this option, click and drag to draw a circular contour. The integral of the currently plotted function along the given circle (with counterclockwise orientation) will be displayed.

251 |
252 | 253 |

Note that custom functions and some of the more intricate built-in functions are not yet supported. 254 | The computed values are generally accurate to the displayed precision unless the function is highly pathological.

255 | 256 | 257 |

Graphics Options

258 |

The following options can be found below the variables panel:

259 | 260 |
261 |

Enable Checkerboard

262 |

Overlays a checkerboard with eight squares per unit length, colored according to the real and imaginary parts of the function’s value.

263 | 264 |

Invert Gradient

265 |

Reverses the direction of the magnitude gradient. When the gradient is inverted, the apparent height of the “layers” formed correlate with the magnitude of the function.

266 | 267 |

Continuous Gradient

268 |

Use a continuous magnitude gradient rather than the “stepped” default. Reduces the range of visually discernible magnitudes, but removes shading artifacts in certain situations.

269 |

Enable Axes

270 |

Overlay coordinate axes and gridlines on the plot. May decrease performance on some devices.

271 |
272 | 273 |

Tips

274 |

To share a graph, just copy and paste the URL! The address automatically changes to reflect the plotted expression.

275 | 276 |

Advanced Features

277 | 278 | }> 279 | 280 | Please read thoroughly iff you are using advanced options. 281 | 282 | 283 | 284 |
285 |

Custom Functions

286 |

This option can be found under the “Advanced Options” heading in the left pane. Overrides the main expression bar, allowing you to code your own function.

287 |

Custom functions use the GLSL programming language. If you've never worked with GLSL before, it's extremely similar to C. Here are a few tips and caveats:

288 |
    289 |
  • Complex numbers are represented by the vec2 type, which represents a 2D vector. The real part can be accessed through z.x and the imaginary part by z.y.
  • 290 |
  • Your custom function should be named mapping, and should take one input of type vec2 and return an output of type vec2.
  • 291 |
  • Complex number literals a+bi should be entered as vec2(a, b), even if they are real. GLSL doesn’t support addition or subtraction between the “complex” vec2 type and floats. Scalar multiplication should work fine with floats, however. For convenience, there are constants ONE and I available.
  • 292 |
  • Addition and subtraction operators (+ and -) work fine between vec2 complex numbers. However, multiplication, division, and exponentiation operators act component-wise on the real and imaginary parts, which is probably not what you want. There are cmul, cdiv, and cpow functions available which will perform the proper complex operation.
  • 293 |
  • All supported functions are available, but with a c prefix. For example, the sin function is named csin. This is to avoid clashing with the built-in (real-valued) GLSL functions.
  • 294 |
  • Constants are available in uppercase with a C_ prefix (e.g. C_PI), and are already in complex vec2 form.
  • 295 |
296 | 297 |

Limitations

298 |

There are currently a few limitations with the custom function feature which I hope to resolve soon:

299 |
    300 |
  • URL-based sharing doesn’t currently work for custom functions. You may share the code directly instead.
  • 301 |
  • The editor will show a red underline if your code fails to compile, but currently does not show useful error information. For now, this information is visible in the developer console.
  • 302 |
  • (Thanks to Tim N. for pointing this out.) Due to low-level limitations of GLSL, variable-length loops such as the following do not work: 303 | {`for (int i=0; i 306 | The following workaround is available: 307 | {`for (int i=0; i<1000; i++) { 308 | if (i 312 | This effectively requires setting an arbitrary but finite limit on the number of iterations you wish to do; 313 | however, this may be acceptable in several applications. 314 | Note that the loop will be unrolled during compilation, so setting an excessively high limit will lead to poor performance. 315 |
  • 316 |
317 | 318 |

Tips

319 |

Most desktop browsers will display a small resize handle at the bottom-right corner of the left pane. This can be very useful when working on complex code.

320 |

You can define arbitrary helper functions before your mapping function — you have the full power of GLSL!

321 | 322 |

You can use all your declared variables directly in your code! They’ve already been converted into complex vec2 form.

323 | 324 |

Obligatory Warning

325 |

This is an advanced feature — you may break things! If anything freezes or crashes, a quick reload should resolve the issue (save your code first)!

326 |
327 |
328 |
329 | 330 |

Citations

331 |

If you use this tool to produce images for a publication, I would greatly appreciate a citation! Something along the lines of:

332 | 333 |

or the equivalent in your journal's citation style is enough.

334 | 335 |

Acknowledgements

336 |
    337 |
  • Inspired by David Bau’s Conformal Map Plotter.
  • 338 |
  • Thanks to Tim N. for helping improve the custom function documentation for variable-length loops.
  • 339 |
  • Thanks to Liu Yi (Ireis, CAS) for requesting the implementation of elliptic functions.
  • 340 |
  • Thanks to Jason Poulin for requesting variable animation support and other improvements.
  • 341 |
  • Thanks to Marius Sarbach for requesting the coordinate axis overlay.
  • 342 |
  • Thanks to Ryan Solecki for requesting the sum, product, and derivative features.
  • 343 |
  • Thanks to Gabriele D'Urso and Ryan Solecki for requesting the product log (Lambert W) function.
  • 344 |
345 | 346 | Complex Function Plotter — Made with love by Samuel J. Li
347 | View the source on GitHub
348 | Found a bug? — bug.report@samuelj.li 349 |
350 | ); 351 | } 352 | } 353 | 354 | export default withStyles(styles)(HelpText); 355 | -------------------------------------------------------------------------------- /src/components/HelpText/help-text.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans'); 2 | 3 | .help-panel { 4 | max-width: 580px; 5 | width: 100vw; 6 | height: 100% !important; 7 | } 8 | 9 | #help-text { 10 | margin: 30px; 11 | } 12 | 13 | #help-text p,li { 14 | font-size: 15px; 15 | line-height: 1.5; 16 | 17 | font-family: 'Roboto', 'Open Sans', sans-serif; 18 | 19 | color: hsl(200, 10%, 10%); 20 | 21 | margin-top: 0; 22 | } 23 | 24 | #help-text li { 25 | margin-bottom: 10px; 26 | } 27 | 28 | #help-text h1, h2, h3 { 29 | font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; 30 | } 31 | 32 | #help-text h1 { 33 | font-weight: 300; 34 | font-size: 32px; 35 | color: hsl(200, 10%, 25%); 36 | 37 | margin-top: 0; 38 | margin-bottom: 10px; 39 | 40 | display: flex; 41 | flex-direction: row; 42 | align-items: center; 43 | } 44 | 45 | #help-text h1 .logo { 46 | width: 60px; 47 | margin-right: 18px; 48 | } 49 | 50 | #help-text h1 .text { 51 | display: flex; 52 | flex-direction: column; 53 | } 54 | 55 | #help-text h1 .text .subtitle { 56 | font-size: 14px; 57 | color: hsl(213, 10%, 30%); 58 | margin-left: 3px; 59 | } 60 | 61 | #help-text h2 { 62 | font-weight: 500; 63 | font-size: 22px; 64 | line-height: 1.25; 65 | color: hsl(200, 10%, 15%); 66 | 67 | margin-top: 40px; 68 | margin-bottom: 10px; 69 | } 70 | 71 | #help-text h3 { 72 | font-weight: 400; 73 | font-size: 13px; 74 | color: hsl(200, 10%, 30%); 75 | 76 | text-transform: uppercase; 77 | letter-spacing: 0.07em; 78 | 79 | margin-top: 20px; 80 | margin-bottom: 3px; 81 | } 82 | 83 | .help-indent { 84 | margin-left: 20px; 85 | } 86 | 87 | /* Media Queries */ 88 | @media (max-width: 600px) { 89 | #help-text h1 { 90 | font-size: 22px; 91 | } 92 | 93 | #help-text h1 .logo { 94 | width: 50px; 95 | margin-right: 15px; 96 | } 97 | 98 | #help-text h2 { 99 | font-size: 18px; 100 | } 101 | 102 | #help-text p,li { 103 | font-size: 14px; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/IntegralPanel.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | 3 | import List from '@material-ui/core/List'; 4 | import ListSubheader from '@material-ui/core/ListSubheader'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 8 | 9 | import FreeformIcon from '@material-ui/icons/Brush'; 10 | import ClosedLoopIcon from '@material-ui/icons/SettingsBackupRestore'; 11 | import CircleIcon from '@material-ui/icons/PanoramaFishEye'; 12 | 13 | 14 | const calculators = [ 15 | { 16 | strategy: 'freeform', 17 | label: 'Freeform', 18 | icon: , 19 | }, 20 | { 21 | strategy: 'freeform-closed', 22 | label: 'Freeform (Closed Loop)', 23 | icon: , 24 | }, 25 | { 26 | strategy: 'circle', 27 | label: 'Circular Contour', 28 | icon: , 29 | }, 30 | ]; 31 | 32 | class IntegralPanel extends PureComponent { 33 | renderIntegrator(entry) { 34 | const {strategy, label, icon} = entry; 35 | const {hidePanels, openCalculator} = this.props; 36 | 37 | const onClick = () => { 38 | // Hide panels on mobile 39 | if (window.innerWidth < 700) {hidePanels();} 40 | 41 | // Close existing calculator, if present 42 | openCalculator(null); 43 | 44 | // Show desired integral calculator 45 | // (setTimeout to ensure old calculator closes) 46 | setTimeout(() => openCalculator(strategy), 0); 47 | }; 48 | 49 | return 50 | {icon} 51 | {label} 52 | ; 53 | } 54 | 55 | render() { 56 | return 57 | Contour Integrals 58 | {calculators.map(this.renderIntegrator.bind(this))} 59 | ; 60 | } 61 | } 62 | 63 | export default IntegralPanel; 64 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/ResultTooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | 4 | import {InlineMath, BlockMath} from 'react-katex'; 5 | 6 | import {formatComplex} from '../util'; 7 | 8 | const PREFIX = '\\int_{\\gamma} f(z) \\, \\mathrm{d}z = '; 9 | 10 | export default ({x, y, style}) => { 11 | let contents =
16 | Custom functions are currently unsupported. 17 |
; 18 | 19 | if (isFinite(x) && isFinite(y)) { 20 | if (window.innerWidth < 700) { 21 | contents =
31 | {PREFIX} 32 | {formatComplex(x, y, 3)} 33 |
; 34 | } else { 35 | contents =
40 | {PREFIX + formatComplex(x, y, 5)} 41 |
; 42 | } 43 | } 44 | 45 | return
53 |
{contents}
54 |
}; 55 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/index.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | 3 | import {isNil} from 'lodash'; 4 | 5 | import {pixelToPlot} from '../FunctionPlot'; 6 | import toJS from '../../gl-code/translators/to-js'; 7 | 8 | import ResultTooltip from './ResultTooltip'; 9 | 10 | import FreeformStrategy from './strategies/Freeform'; 11 | import ClosedFreeformStrategy from './strategies/FreeformClosed'; 12 | import CircleStrategy from './strategies/Circle'; 13 | 14 | const STRATEGY_DICT = { 15 | 'freeform': FreeformStrategy, 16 | 'freeform-closed': ClosedFreeformStrategy, 17 | 'circle': CircleStrategy, 18 | } 19 | 20 | class IntegralCalculator extends PureComponent { 21 | state = { 22 | done: false, 23 | 24 | mouseDown: false, 25 | mousePosition: null, 26 | strategy: null, 27 | } 28 | 29 | constructor(props) { 30 | super(props); 31 | 32 | // Create canvas ref 33 | this.canvas = React.createRef(); 34 | } 35 | 36 | resizeCanvas() { 37 | const canvas = this.canvas.current; 38 | const dpr = window.devicePixelRatio; 39 | canvas.width = canvas.offsetWidth * dpr; 40 | canvas.height = canvas.offsetHeight * dpr; 41 | } 42 | 43 | 44 | // Re-initialized the state if dirty 45 | resetState() { 46 | const {done} = this.state; 47 | if (done) { 48 | this.setState({ 49 | done: false, 50 | mousePosition: null, 51 | strategy: null, 52 | }); 53 | } 54 | } 55 | 56 | handleMouseDown(e) { 57 | const {integrator, expression, variables, onClose} = this.props; 58 | const {done} = this.state; 59 | 60 | if (done) { 61 | // Close if already integrated once 62 | onClose(); 63 | } else { 64 | // Compile plotted expression to JS 65 | const mapping = toJS(expression, variables); 66 | 67 | // Clear canvas 68 | this.resizeCanvas(); 69 | const context = this.canvas.current.getContext('2d'); 70 | context.clearRect(0, 0, context.canvas.width, context.canvas.height); 71 | 72 | // Start integrating 73 | this.setState({ 74 | mouseDown: true, 75 | strategy: new STRATEGY_DICT[integrator]( 76 | mapping, context, window.devicePixelRatio, 77 | ), 78 | }); 79 | } 80 | } 81 | 82 | handleMouseMove(e) { 83 | const {strategy, mouseDown} = this.state; 84 | const {variables} = this.props; 85 | 86 | if (!mouseDown) {return;} 87 | 88 | const {clientX, clientY} = e; 89 | const [x, y] = pixelToPlot(clientX, clientY, variables); 90 | 91 | this.setState({mousePosition: [clientX, clientY]}); 92 | 93 | if (!isNil(strategy)) { 94 | const dpr = window.devicePixelRatio; 95 | strategy.update({ 96 | x, y, 97 | clientX: clientX * dpr, 98 | clientY: clientY * dpr, 99 | }); 100 | } 101 | } 102 | 103 | handleMouseUp(e) { 104 | const {strategy} = this.state; 105 | 106 | // Notify integrator 107 | strategy.finish(); 108 | 109 | // Set finished state 110 | this.setState({mouseDown: false, done: true}); 111 | } 112 | 113 | render() { 114 | const {integrator} = this.props; 115 | const {done, mouseDown, mousePosition, strategy} = this.state; 116 | 117 | if (isNil(integrator)) { 118 | this.resetState(); 119 | return null; 120 | } 121 | 122 | let tooltip = null; 123 | if (!(isNil(strategy) || isNil(mousePosition))) { 124 | const [x, y] = strategy.value(); 125 | 126 | const snap = x => (Math.abs(x) < 1e-10 ? 0 : x); 127 | 128 | tooltip = ; 137 | } 138 | 139 | 140 | return
this.handleMouseMove(e.touches[0])} 147 | onTouchEnd={this.handleMouseUp.bind(this)} 148 | > 149 | 153 | {tooltip} 154 |

155 | Click and drag to draw a contour 156 |

157 | 200 |
; 201 | } 202 | } 203 | 204 | export default IntegralCalculator; 205 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/strategies/Circle.js: -------------------------------------------------------------------------------- 1 | import Strategy from './Strategy'; 2 | 3 | import {isNil} from 'lodash'; 4 | 5 | import {integrateReal} from './util'; 6 | 7 | // Kahan summation algorithm 8 | function kahanSum(list) { 9 | let sum = 0; 10 | let c = 0; 11 | 12 | for (let entry of list) { 13 | const y = entry - c; 14 | const t = sum + y; 15 | c = (t - sum) - y; 16 | sum = t; 17 | } 18 | 19 | return sum; 20 | } 21 | 22 | 23 | class Circle extends Strategy { 24 | center = null; 25 | radius = null; 26 | 27 | pageCenter = null; 28 | pageRadius = null; 29 | 30 | integrate(x, y) { 31 | if (isNil(this.center)) {this.center = [x, y];} 32 | this.radius = Math.hypot(x - this.center[0], y - this.center[1]); 33 | } 34 | 35 | draw(x, y) { 36 | if (isNil(this.pageCenter)) {this.pageCenter = [x, y];} 37 | this.pageRadius = Math.hypot(x - this.pageCenter[0], y - this.pageCenter[1]); 38 | 39 | this.canvas.clearRect(0, 0, this.canvas.canvas.width, this.canvas.canvas.height); 40 | this.canvas.beginPath(); 41 | this.canvas.arc(...this.pageCenter, this.pageRadius, 0, 2 * Math.PI); 42 | this.canvas.stroke(); 43 | } 44 | 45 | value() { 46 | // Return 0 if not initialized 47 | if (isNil(this.center)) {return [0, 0];} 48 | const [x, y] = this.center; 49 | 50 | // Parameterize circle and compute pullback of integrand 51 | const gamma = theta => [ 52 | x + this.radius * Math.cos(theta), 53 | y + this.radius * Math.sin(theta), 54 | ]; 55 | const gammaPrime = theta => [ 56 | -this.radius * Math.sin(theta), 57 | this.radius * Math.cos(theta), 58 | ]; 59 | const integrand = theta => { 60 | const [u, v] = this.mapping(gamma(theta)); 61 | const [up, vp] = gammaPrime(theta); 62 | return [u * up - v * vp, u * vp + v * up]; 63 | } 64 | 65 | // Divide circle into arcs and integrate along each 66 | const N = 32; 67 | const scaleFactor = 2 * Math.PI / N; 68 | const summands = [[], []]; 69 | for (let i=0; i < N; i++) { 70 | const [u, v] = integrateReal( 71 | i * scaleFactor, (i + 1) * scaleFactor, integrand 72 | ); 73 | summands[0].push(u); 74 | summands[1].push(v); 75 | } 76 | 77 | return [kahanSum(summands[0]), kahanSum(summands[1])]; 78 | } 79 | } 80 | 81 | export default Circle; 82 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/strategies/Freeform.js: -------------------------------------------------------------------------------- 1 | import Strategy from './Strategy'; 2 | 3 | import {isNil} from 'lodash'; 4 | 5 | import {integrateSegment} from './util'; 6 | 7 | 8 | class FreeformIntegrator extends Strategy { 9 | accumulator = [0, 0]; 10 | 11 | lastPoint = null; 12 | lastPagePoint = null; 13 | 14 | integrate(x, y) { 15 | if (!isNil(this.lastPoint)) { 16 | // Integrate along segment between last and current points 17 | const df = integrateSegment(this.lastPoint, [x, y], this.mapping); 18 | this.accumulator[0] += df[0] 19 | this.accumulator[1] += df[1]; 20 | } 21 | this.lastPoint = [x, y]; 22 | } 23 | 24 | draw(x, y) { 25 | if (!isNil(this.lastPagePoint)) { 26 | const [lastX, lastY] = this.lastPagePoint; 27 | this.canvas.beginPath(); 28 | this.canvas.moveTo(lastX, lastY); 29 | this.canvas.lineTo(x, y); 30 | this.canvas.stroke(); 31 | } 32 | this.lastPagePoint = [x, y]; 33 | } 34 | 35 | value() { 36 | return this.accumulator; 37 | } 38 | } 39 | 40 | export default FreeformIntegrator; 41 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/strategies/FreeformClosed.js: -------------------------------------------------------------------------------- 1 | import Freeform from './Freeform'; 2 | 3 | import {isNil} from 'lodash'; 4 | 5 | class FreeformClosed extends Freeform { 6 | startPoint = null; 7 | startPagePoint = null; 8 | 9 | integrate(x, y) { 10 | super.integrate(x, y); 11 | if (isNil(this.startPoint)) { 12 | this.startPoint = [x, y]; 13 | } 14 | } 15 | 16 | draw(x, y) { 17 | super.draw(x, y); 18 | if (isNil(this.startPagePoint)) { 19 | this.startPagePoint = [x, y]; 20 | } 21 | } 22 | 23 | finish() { 24 | if (isNil(this.startPoint) || isNil(this.startPagePoint)) {return;} 25 | 26 | // Loop back to start 27 | const [x, y] = this.startPoint; 28 | const [clientX, clientY] = this.startPagePoint; 29 | this.update({ 30 | x, y, 31 | clientX, clientY 32 | }); 33 | } 34 | } 35 | 36 | export default FreeformClosed; 37 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/strategies/Strategy.js: -------------------------------------------------------------------------------- 1 | class Strategy { 2 | constructor(mapping, canvas, dpr) { 3 | this.mapping = mapping; 4 | this.canvas = canvas; 5 | this.dpr = dpr; 6 | 7 | this.initialize(); 8 | } 9 | 10 | initialize() { 11 | this.canvas.strokeStyle = 'white'; 12 | this.canvas.lineWidth = 2 * this.dpr; 13 | } 14 | 15 | update(data) { 16 | const {x, y, clientX, clientY} = data; 17 | this.integrate(x, y); 18 | this.draw(clientX, clientY); 19 | } 20 | 21 | finish() {} 22 | 23 | integrate(x, y) {} 24 | 25 | draw(x, y) {} 26 | 27 | value() { 28 | return [0, 0]; 29 | } 30 | } 31 | 32 | export default Strategy; 33 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/strategies/Test.js: -------------------------------------------------------------------------------- 1 | import Strategy from './Strategy'; 2 | 3 | class TestIntegrator extends Strategy { 4 | count = 0; 5 | 6 | update(data) { 7 | this.count++; 8 | } 9 | 10 | value() { 11 | return [this.count, 1]; 12 | } 13 | } 14 | 15 | export default TestIntegrator; 16 | -------------------------------------------------------------------------------- /src/components/IntegralCalculator/strategies/util.js: -------------------------------------------------------------------------------- 1 | // 16-point Gaussian quadrature 2 | const GAUSS_COEFFICIENTS = [ 3 | [0.1894506104550685, 0.0950125098376374], 4 | [0.1826034150449236, 0.2816035507792589], 5 | [0.1691565193950025, 0.4580167776572274], 6 | [0.1495959888165767, 0.6178762444026438], 7 | [0.1246289712555339, 0.7554044083550030], 8 | [0.0951585116824928, 0.8656312023878318], 9 | [0.0622535239386479, 0.9445750230732326], 10 | [0.0271524594117541, 0.9894009349916499], 11 | 12 | [0.1894506104550685, -0.0950125098376374], 13 | [0.1826034150449236, -0.2816035507792589], 14 | [0.1691565193950025, -0.4580167776572274], 15 | [0.1495959888165767, -0.6178762444026438], 16 | [0.1246289712555339, -0.7554044083550030], 17 | [0.0951585116824928, -0.8656312023878318], 18 | [0.0622535239386479, -0.9445750230732326], 19 | [0.0271524594117541, -0.9894009349916499], 20 | ]; 21 | 22 | // Integrates a function R -> C 23 | // from start to end via 24 | // Gaussian quadrature. 25 | function integrateReal(start, end, mapping) { 26 | // Reparameterize to [-1, 1] 27 | const dx = end - start; 28 | const submap = t => mapping( 29 | start + 0.5 * dx * (1 + t) 30 | ); 31 | 32 | // Estimate average value of f over the given interval 33 | const result = [0, 0]; 34 | for (let [weight, abscissa] of GAUSS_COEFFICIENTS) { 35 | const [u, v] = submap(abscissa); 36 | result[0] += u * dx * weight / 2; 37 | result[1] += v * dx * weight / 2; 38 | } 39 | return result; 40 | } 41 | 42 | // Integrates f(z) dz along the given line segment. 43 | // Start and end are 2-tuples representing 44 | // the start and end points in the complex plane. 45 | // Mapping is a function [x, y] -> [u, v] 46 | // representing f. 47 | function integrateSegment(start, end, mapping) { 48 | const [x0, y0] = start; 49 | const [x1, y1] = end; 50 | 51 | // Compute dz 52 | const dx = x1 - x0; 53 | const dy = y1 - y0; 54 | 55 | // Approximate average value of f(z) 56 | // along the segment by Gaussian quadrature 57 | const submap = t => mapping([ 58 | x0 + t * dx, 59 | y0 + t * dy, 60 | ]); 61 | const [u, v] = integrateReal(0, 1, submap); 62 | 63 | return [ 64 | u * dx - v * dy, 65 | v * dx + u * dy, 66 | ]; 67 | } 68 | 69 | export {integrateReal, integrateSegment}; 70 | -------------------------------------------------------------------------------- /src/components/OptionsPanel/OptionsPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import List from '@material-ui/core/List'; 4 | import ListSubheader from '@material-ui/core/ListSubheader'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | 9 | 10 | class BooleanOption extends React.PureComponent { 11 | render() { 12 | const {onClick, checked, label} = this.props; 13 | return ( 14 | 18 | 21 | {label} 22 | 23 | ); 24 | } 25 | } 26 | 27 | class OptionsPanel extends React.PureComponent { 28 | renderOptions() { 29 | const options = []; 30 | for (const [variable, label] of Object.entries(this.props.options)) { 31 | options.push( 32 | this.props.onToggle(variable)} 35 | checked={this.props.variables[variable] > 0.5} 36 | label={label} 37 | /> 38 | ); 39 | } 40 | return options; 41 | } 42 | 43 | render() { 44 | return ( 45 | 46 | {this.props.heading} 47 | {this.renderOptions()} 48 | 49 | ); 50 | } 51 | } 52 | 53 | export default OptionsPanel; 54 | -------------------------------------------------------------------------------- /src/components/SidePanel/SidePanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | 5 | import Drawer from '@material-ui/core/Drawer'; 6 | import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'; 7 | 8 | import './side-panel.css'; 9 | 10 | 11 | const styles = theme => ({ 12 | toolbar: theme.mixins.toolbar 13 | }); 14 | 15 | class SidePanel extends React.PureComponent { 16 | constructor(props) { 17 | super(props); 18 | this.classes = props.classes; 19 | this.state = { 20 | width: null 21 | } 22 | } 23 | 24 | componentDidMount() { 25 | this.updateWidth(); 26 | window.addEventListener('resize', this.updateWidth.bind(this)); 27 | } 28 | 29 | updateWidth() { 30 | this.setState({width: window.innerWidth}); 31 | } 32 | 33 | render() { 34 | if (this.state.width > this.props.transitionWidth) { 35 | return ( 36 | 44 |
45 |
46 | {this.props.children} 47 |
48 | 49 | ); 50 | } else { 51 | return ( 52 | 63 |
64 | {this.props.children} 65 |
66 |
67 | ); 68 | } 69 | } 70 | } 71 | 72 | export default withStyles(styles)(SidePanel); 73 | -------------------------------------------------------------------------------- /src/components/SidePanel/side-panel.css: -------------------------------------------------------------------------------- 1 | .drawer-content { 2 | overflow-x: hidden; 3 | background-color: white; 4 | } 5 | 6 | .MuiList-root { 7 | background-color: white; 8 | opacity: 1; 9 | transition: opacity 0.2s ease-in-out; 10 | } 11 | 12 | @media (max-width: 480px) { 13 | .drawer-content { 14 | background-color: unset; 15 | } 16 | 17 | .MuiList-root { 18 | background-color: unset; 19 | } 20 | 21 | .control-panel.changing .MuiList-root:not(.slider-list) { 22 | opacity: 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/SliderPanel/SliderPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import List from '@material-ui/core/List'; 4 | import ListSubheader from '@material-ui/core/ListSubheader'; 5 | 6 | import VariableAdder from './VariableAdder.js'; 7 | 8 | import Slider from './slider/Slider.js'; 9 | 10 | import './slider-panel.css'; 11 | 12 | 13 | const hiddenVariables = new Set([ 14 | 'log_scale', 'center_x', 'center_y', 15 | 16 | 'enable_checkerboard', 17 | 'invert_gradient', 18 | 'continuous_gradient', 19 | 'enable_axes', 20 | 21 | 'custom_function' 22 | ]); 23 | 24 | class SliderPanel extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | this.textField = React.createRef(); 28 | this.changing = {} 29 | } 30 | 31 | handleChange(name, value) { 32 | this.props.variables[name] = value; 33 | this.props.onUpdate({[name]: value}); 34 | this.forceUpdate(); 35 | } 36 | 37 | handleSetChanging(name, v) { 38 | this.changing[name] = v; 39 | this.props.setChanging(Object.keys(this.props.variables).some((x) => this.changing[x])); 40 | } 41 | 42 | renderSliderList() { 43 | const {variables} = this.props; 44 | let sliderList = []; 45 | for (const [name, value] of Object.entries(variables)) { 46 | // Skip variables in hidden blacklist 47 | if (hiddenVariables.has(name)) {continue;} 48 | 49 | // Return slider component 50 | sliderList.push( 51 | this.handleChange(name, value)} 56 | onDelete={() => this.props.onRemove(name)} 57 | setChanging={(v) => this.handleSetChanging(name, v)} 58 | /> 59 | ); 60 | } 61 | 62 | return sliderList; 63 | } 64 | 65 | render() { 66 | return ( 67 | Variables 68 | {this.renderSliderList()} 69 | this.props.onAdd(name, 0.5)} 71 | /> 72 | 73 | ); 74 | } 75 | } 76 | 77 | export {hiddenVariables}; 78 | export default SliderPanel; 79 | -------------------------------------------------------------------------------- /src/components/SliderPanel/VariableAdder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | 4 | import functionList from '../../gl-code/function-list.js'; 5 | 6 | import ListItem from '@material-ui/core/ListItem'; 7 | import TextField from '@material-ui/core/TextField'; 8 | import Button from '@material-ui/core/Button'; 9 | 10 | 11 | const blacklist = functionList; 12 | 13 | class VariableAdder extends React.PureComponent { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | value: '', 18 | errorMessage: '' 19 | }; 20 | } 21 | 22 | setValue(value) { 23 | this.setState({value: value, errorMessage: ''}); 24 | } 25 | 26 | handleKeyPress(event) { 27 | if (event.keyCode === 13) { 28 | this.handleClick(); 29 | } 30 | } 31 | 32 | handleClick() { 33 | const name = this.state.value.toLowerCase(); 34 | if (name === '') { 35 | this.setState({errorMessage: 'Must be nonempty'}); 36 | } else if (name.match(/^[a-z]+$/) === null) { 37 | this.setState({errorMessage: 'Letters only'}); 38 | } else if (blacklist.has(name)) { 39 | this.setState({errorMessage: 'Restricted keyword'}); 40 | } else { 41 | this.setValue(''); 42 | this.props.onClick(name); 43 | } 44 | } 45 | 46 | render() { 47 | return ( 48 | 52 | this.setValue(event.target.value)} 55 | onKeyDown={(event) => this.handleKeyPress(event)} 56 | error={this.state.errorMessage !== ''} 57 | helperText={this.state.errorMessage} 58 | value={this.state.value} 59 | className={css` 60 | flex-grow: 1; 61 | `} 62 | /> 63 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | export default VariableAdder; 72 | -------------------------------------------------------------------------------- /src/components/SliderPanel/VariableSlider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | import Slider from '@material-ui/core/Slider'; 11 | 12 | import DeleteIcon from '@material-ui/icons/Delete'; 13 | 14 | 15 | const styles = theme => ({ 16 | variableSlider: { 17 | paddingLeft: 0, 18 | paddingRight: 0 19 | } 20 | }); 21 | 22 | function VariableSlider({onChange, name, value, onDelete, classes}) { 23 | return ( 24 | 25 |
26 | 27 | {name + ' = ' + value.toFixed(3)} 28 | 29 | 37 |
38 | 39 | 40 | 41 | 42 | 43 |
44 | ); 45 | } 46 | 47 | export default withStyles(styles)(VariableSlider); 48 | -------------------------------------------------------------------------------- /src/components/SliderPanel/editable-value/EditableValue.css: -------------------------------------------------------------------------------- 1 | .editable-value { 2 | color: #777; 3 | padding: 0; 4 | border: 0; 5 | 6 | transition: color 0.1s; 7 | } 8 | 9 | .editable-value:hover { 10 | color: #444; 11 | } 12 | 13 | .editable-value.focus { 14 | color: black; 15 | } 16 | 17 | .editable-value.invalid { 18 | color: red; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/SliderPanel/editable-value/EditableValue.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import ContentEditable from 'react-contenteditable'; 3 | 4 | import DEFAULT_PARSERS from './parsers.js'; 5 | import './EditableValue.css'; 6 | 7 | 8 | class EditableValue extends PureComponent { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | focused: false, 13 | text: '' 14 | }; 15 | this.element = React.createRef(); 16 | } 17 | 18 | /*** Value Parsing ***/ 19 | getParser() { 20 | if (this.props.parser in DEFAULT_PARSERS) { 21 | return DEFAULT_PARSERS[this.props.parser]; 22 | } else { 23 | return this.props.parser; 24 | } 25 | } 26 | 27 | parsedValue() { 28 | return this.getParser()(this.state.text); 29 | } 30 | 31 | valid() { 32 | return this.parsedValue() !== undefined; 33 | } 34 | 35 | // Resets this.state.text to match this.props.value 36 | reset() { 37 | const propText = this.props.value.toString(); 38 | if (propText !== this.state.text) { 39 | this.setState({text: this.props.value.toString()}); 40 | } 41 | } 42 | 43 | 44 | /*** Event Handlers ***/ 45 | componentDidMount() { 46 | // Exit on Enter keypress 47 | document.addEventListener('keydown', e => { 48 | if (e.key === 'Enter' && this.state.focused) { 49 | this.element.current.blur(); 50 | } 51 | }); 52 | } 53 | 54 | handleFocus(e) { 55 | this.setState({focused: true}); 56 | this.reset(); 57 | } 58 | 59 | handleChange(e) { 60 | this.setState({text: e.target.value.toString()}); 61 | } 62 | 63 | handleBlur(e) { 64 | this.setState({focused: false}); 65 | 66 | if (this.valid()) { 67 | this.props.onChange(this.parsedValue()); 68 | } 69 | } 70 | 71 | componentDidUpdate() {if (!this.state.focused) {this.reset();}} 72 | 73 | render() { 74 | const classes = ['editable-value', this.props.name]; 75 | 76 | if (this.state.focused) { 77 | classes.push('focus') 78 | if (!this.valid()) { 79 | classes.push('invalid') 80 | }; 81 | } 82 | 83 | const classString = classes.join(' '); 84 | const current = this.element.current; 85 | if (current !== null) { 86 | this.element.current.setAttribute('class', classString); 87 | } 88 | 89 | const displayText = this.state.focused ? this.state.text : this.props.value.toString(); 90 | 91 | if (this.props.disabled) { 92 | return ( 93 | 94 | {displayText} 95 | 96 | ); 97 | } else { 98 | return ( 99 | 112 | ); 113 | } 114 | } 115 | } 116 | 117 | export default EditableValue; 118 | -------------------------------------------------------------------------------- /src/components/SliderPanel/editable-value/parsers.js: -------------------------------------------------------------------------------- 1 | function lowerParser(text) { 2 | return text.toLowerCase(); 3 | } 4 | 5 | function numberParser(text) { 6 | const parsed = Number(text); 7 | 8 | if (!Number.isNaN(parsed)) { 9 | return parsed; 10 | } 11 | } 12 | 13 | const parsers = { 14 | number: numberParser, 15 | lower: lowerParser 16 | }; 17 | export default parsers; 18 | -------------------------------------------------------------------------------- /src/components/SliderPanel/slider-panel.css: -------------------------------------------------------------------------------- 1 | div.variable-slider { 2 | width: 90%; 3 | } 4 | 5 | @media (max-width: 480px) { 6 | .control-panel.changing .variable-adder { 7 | opacity: 0; 8 | } 9 | 10 | .control-panel.changing .MuiListSubheader-root { 11 | opacity: 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/SliderPanel/slider/Slider.css: -------------------------------------------------------------------------------- 1 | .slider-base { 2 | width: 100%; 3 | display: flex; 4 | white-space: nowrap; 5 | 6 | margin: 16px 0; 7 | margin-bottom: 24px; 8 | padding: 0 0 0 25px; 9 | 10 | box-sizing: border-box; 11 | } 12 | 13 | .slider { 14 | flex-grow: 1; 15 | } 16 | 17 | .slider-base .delete-icon { 18 | padding: 0; 19 | margin: auto 8px; 20 | width: 40px; 21 | height: 40px; 22 | } 23 | 24 | /*** Info Row ***/ 25 | .slider .info-row { 26 | white-space: nowrap; 27 | display: flex; 28 | 29 | width: 100%; 30 | height: 20px; 31 | } 32 | 33 | .slider .info-row { 34 | font-size: 16px; 35 | font-weight: 500; 36 | font-family: 'KaTeX_Main', 'Roboto', 'Helvetica', 'Arial', sans-serif; 37 | line-height: 20px; 38 | } 39 | 40 | .slider .info-row .editable-value { 41 | color: black; 42 | } 43 | 44 | .slider .info-row .editable-value.name-italic { 45 | font-style: italic; 46 | font-family: 'KaTeX_Math'; 47 | margin-right: 4px; 48 | } 49 | 50 | .slider .info-row .editable-value.invalid { 51 | color: red; 52 | } 53 | 54 | .slider .info-row .name { 55 | margin-right: 3px; 56 | } 57 | 58 | .slider .info-row .value { 59 | margin-left: 3px; 60 | } 61 | 62 | /*** Main Row ***/ 63 | .slider .main-row { 64 | white-space: nowrap; 65 | display: flex; 66 | 67 | width: 100%; 68 | height: 30px; 69 | } 70 | 71 | .slider .main-row .editable-value { 72 | font-size: 16px; 73 | font-family: 'KaTeX_Main', 'Roboto', 'Helvetica', 'Arial', sans-serif; 74 | line-height: 30px; 75 | } 76 | 77 | .slider .main-row .main-slider { 78 | margin: auto 10px; 79 | padding: 0; 80 | flex-grow: 1; 81 | } 82 | 83 | @media (max-width: 480px) { 84 | .control-panel.changing .slider-wrapper { 85 | box-shadow: 1px 1px 4px hsla(200, 10%, 10%, 0.3); 86 | } 87 | 88 | .slider .info-row { 89 | padding-top: 8px; 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /src/components/SliderPanel/slider/Slider.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { css } from '@emotion/css'; 3 | 4 | 5 | import BaseSlider from '@material-ui/core/Slider'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import DeleteIcon from '@material-ui/icons/Delete'; 8 | 9 | import PlayIcon from '@material-ui/icons/PlayArrow'; 10 | import BounceIcon from '@material-ui/icons/SyncAlt'; 11 | import LoopIcon from '@material-ui/icons/ArrowRightAlt'; 12 | 13 | import EditableValue from '../editable-value/EditableValue.js'; 14 | 15 | import './Slider.css'; 16 | 17 | const SPEEDS = [ 18 | 0.2, 19 | 0.5, 20 | 1, 21 | 2, 22 | 3, 23 | 4 24 | ]; 25 | 26 | function AnimationMode({mode, speed, onChangeMode, onChangeSpeed}) { 27 | const nextMode = { 28 | '': 'bounce', 29 | 'bounce': 'loop', 30 | 'loop': '', 31 | }; 32 | const icons = { 33 | '': PlayIcon, 34 | 'bounce': BounceIcon, 35 | 'loop': LoopIcon, 36 | } 37 | const Icon = icons[mode]; 38 | 39 | return
46 | { 51 | const newMode = nextMode[mode]; 52 | onChangeMode(newMode) ; 53 | }}/> 54 | { 56 | onChangeSpeed((speed + 1) % SPEEDS.length); 57 | }} 58 | className={css` 59 | font-size: 15px; 60 | color: hsl(200, 20%, 40%); 61 | cursor: pointer; 62 | font-family: 'KaTeX_Main'; 63 | user-select: none; 64 | 65 | @media (max-width: 480px) { 66 | font-size: 14px; 67 | } 68 | `} 69 | > 70 | {SPEEDS[speed].toFixed(1)}× 71 | 72 |
; 73 | } 74 | 75 | 76 | class Slider extends PureComponent { 77 | state = { 78 | min: 0, 79 | max: 1, 80 | animationMode: '', 81 | animationSpeed: 2, 82 | animationCurrentDirection: 1, 83 | changing: false, 84 | }; 85 | deleted = false; 86 | 87 | 88 | setMin(min) { 89 | const {value, onChange} = this.props; 90 | this.setState({min}); 91 | if (value < min) {onChange(min);} 92 | } 93 | 94 | setMax(max) { 95 | const {value, onChange} = this.props; 96 | this.setState({max}); 97 | if (value > max) {onChange(max);} 98 | } 99 | 100 | setValue(value) { 101 | this.ensureConsistency(); 102 | this.props.onChange(value); 103 | } 104 | 105 | // Adjust this.state.min and this.state.max 106 | // if this.props.value is out of bounds. 107 | ensureConsistency() { 108 | const [min, max, value] = [this.state.min, this.state.max, this.props.value]; 109 | if (min > value) {this.setState({min: value});} 110 | if (max < value) {this.setState({max: value});} 111 | } 112 | 113 | runAnimationTick(timestamp) { 114 | const {onChange, value} = this.props; 115 | const { 116 | min, max, 117 | animationMode, animationCurrentDirection, animationSpeed 118 | } = this.state; 119 | 120 | if (this.deleted) {return;} 121 | 122 | let dt = 0; 123 | if (timestamp !== undefined && this.lastAnimationTick !== undefined) { 124 | dt = timestamp - this.lastAnimationTick; 125 | } 126 | this.lastAnimationTick = timestamp; 127 | 128 | const delta = (max - min) * SPEEDS[animationSpeed] * animationCurrentDirection * dt/4e3; 129 | let newValue = value + delta; 130 | 131 | if (animationMode === 'bounce') { 132 | if (newValue > max) { 133 | newValue = max; 134 | this.setState({animationCurrentDirection: -1}); 135 | } 136 | if (newValue < min) { 137 | newValue = min; 138 | this.setState({animationCurrentDirection: 1}); 139 | } 140 | } 141 | 142 | if (animationMode === 'loop') { 143 | if (newValue > max) {newValue = min;} 144 | } 145 | 146 | const digits = Math.max(-Math.floor(Math.log10(max-min))+3, 3); 147 | const scale = Math.pow(10, digits); 148 | onChange(Math.round(scale * newValue)/scale); 149 | 150 | if (animationMode === '') {return;} 151 | if (this._mounted) { 152 | requestAnimationFrame(this.runAnimationTick.bind(this)); 153 | } 154 | } 155 | 156 | componentDidMount() { 157 | this.ensureConsistency(); 158 | this._mounted = true; 159 | } 160 | 161 | componentWillUnmount() { 162 | this._mounted = false; 163 | this.props.setChanging(false); 164 | } 165 | 166 | handleCommit() { 167 | this.setState({changing: false}); 168 | this.props.setChanging(this.state.animationMode !== ''); 169 | } 170 | 171 | render() { 172 | const {min, max, animationMode, animationSpeed, changing} = this.state; 173 | return ( 174 |
187 | { 192 | this.state.animationMode = mode; // To avoid race conditions 193 | this.setState({animationMode: mode, animationCurrentDirection: 1}); 194 | this.runAnimationTick(); 195 | this.props.setChanging(mode !== ''); 196 | } 197 | } 198 | onChangeSpeed={ 199 | (speed) => { 200 | this.setState({animationSpeed: speed}); 201 | } 202 | } 203 | /> 204 |
205 |
206 | 213 | = 214 | 220 |
221 |
222 | 228 | { 235 | if (!this.state.changing) {this.props.setChanging(true);} 236 | this.state.changing = true; 237 | this.setValue(v); 238 | }} 239 | /> 240 | 246 |
247 |
248 | { 249 | this.deleted = true; 250 | this.props.onDelete(); 251 | }} className='delete-icon'> 252 | 253 | 254 |
255 | ); 256 | } 257 | } 258 | 259 | export default Slider; 260 | -------------------------------------------------------------------------------- /src/components/control-panel.css: -------------------------------------------------------------------------------- 1 | .control-panel { 2 | width: 330px; 3 | height: 100% !important; 4 | resize: horizontal; 5 | background-color: white; 6 | } 7 | 8 | 9 | /* Media Queries */ 10 | 11 | @media (max-width: 480px) { 12 | .control-panel { 13 | width: 100%; 14 | } 15 | 16 | .MuiBackdrop-root { 17 | background-color: unset !important; 18 | } 19 | 20 | .control-panel.changing { 21 | background-color: rgba(0, 0, 0, 0); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/util.js: -------------------------------------------------------------------------------- 1 | import {isNil} from 'lodash'; 2 | 3 | function formatComplex(x, y, precision) { 4 | if (x !== 0) { 5 | if (y !== 0) { 6 | return formatReal(x, false, precision) + formatReal(y, true, precision) + '\\, i'; 7 | } else { 8 | return formatReal(x, false, precision); 9 | } 10 | } else if (y !== 0) { 11 | return formatReal(y, false, precision) + '\\, i'; 12 | } else { 13 | return '0'; 14 | } 15 | } 16 | 17 | const CONSTANTS = { 18 | '\\pi': Math.PI, 19 | '\\sqrt{2}': Math.sqrt(2), 20 | 'e': Math.E, 21 | } 22 | 23 | function almostInteger(x) { 24 | return Math.abs(x - Math.round(x)) < 1e-10; 25 | } 26 | 27 | // Checks if the value is close to a nice multiple of known constants. 28 | function checkKnown(x) { 29 | for (let [name, value] of Object.entries(CONSTANTS)) { 30 | for (let denominator of [1, 2, 3, 6, 12]) { 31 | const ratio = denominator * x / value; 32 | 33 | // Check if ratio is almost integer 34 | if (ratio > 0.5 && almostInteger(ratio)) { 35 | console.log(ratio); 36 | const formatInteger = x => x === 1 ? '' : x.toString() 37 | const numerator = formatInteger(Math.round(ratio)) + ' ' + name; 38 | 39 | if (denominator === 1) { 40 | return numerator; 41 | } else { 42 | return '\\frac{' + numerator + '}{' + denominator + '}'; 43 | } 44 | } 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | function formatReal(x, forceSign, precision) { 52 | const magnitude = Math.abs(x); 53 | const sign = x < 0 ? '-' : ( 54 | forceSign ? '+' : '' 55 | ); 56 | 57 | if (precision > 3) { 58 | // Try to see if value is a multiple of known constant 59 | const prettyMagnitude = checkKnown(magnitude); 60 | if (!isNil(prettyMagnitude)) {return sign + prettyMagnitude;} 61 | } 62 | 63 | let exponent = magnitude > 0 ? Math.floor(Math.log10(magnitude)) : 0; 64 | if (Math.abs(exponent) < 3) {exponent = 0;} 65 | 66 | const mantissa = magnitude * Math.pow(10, -exponent); 67 | 68 | const formattedMagnitude = mantissa.toFixed(precision); 69 | const formattedExponent = exponent === 0 ? '' : `\\times 10^{${exponent}}`; 70 | 71 | return sign + formattedMagnitude + formattedExponent; 72 | } 73 | 74 | export {formatComplex}; 75 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgxli/complex-function-plotter/f127a55d73adc34335091bbe254c797ac3d7ce91/src/favicon.png -------------------------------------------------------------------------------- /src/gl-code/README.md: -------------------------------------------------------------------------------- 1 | To add a function: 2 | - Add syntax to `grammar.ne` and compile grammar. 3 | - Write definition in `complex-functions.js` and add to function list at bottom. 4 | - Add JS implementation in `translators/to-js.js`. 5 | - (If applicable) add derivative rules in `translators/derivative.js`. 6 | - (If applicable) add optimizations to `translators/compiler.js`. 7 | 8 | There are two separate number systems in use; the `vec2` system `x + iy`, and the `vec3` system `e^z (x + iy)`. Functions may have two different definitions in each system (the first for `vec2`, and the second for `vec3`). The `VEC_TYPE` declaration is automatically set to the appropriate type. 9 | 10 | In the `vec3` system, addition and subtraction must be done with the appropriately named functions unless `z = 0` (the number is 'downconverted'). Be careful about downconversion, as it results in a loss of range. The definitions of `cadd`, `cadd4`, `cadd8` treat this properly. 11 | Note that `cmul`, `cdiv`, `creciprocal` preserve downconverted-ness. Output of `clog` is already downconverted. 12 | -------------------------------------------------------------------------------- /src/gl-code/function-list.js: -------------------------------------------------------------------------------- 1 | import {complex_functions} from './complex-functions.js'; 2 | 3 | const restrictedFunctions = []; 4 | 5 | restrictedFunctions.push(...Object.keys(complex_functions)); 6 | restrictedFunctions.push(...Object.values(complex_functions).map(f => f.name)); 7 | restrictedFunctions.push('z', 'i', 'e', 'pi', 'tau', 'phi'); 8 | 9 | /* GLSL Keywords */ 10 | restrictedFunctions.push( 11 | 'attribute', 'const', 'uniform', 'varying', 12 | 'layout', 13 | 'centroid', 'flat', 'smooth', 'noperspective', 14 | 'break', 'continue', 'do', 'for', 'while', 'switch', 'case', 'default', 15 | 'if', 'else', 16 | 'in', 'out', 'inout', 17 | 'float', 'int', 'void', 'bool', 'true', 'false', 18 | 'invariant', 'discard', 'return', 19 | 'lowp', 'mediump', 'highp', 'precision', 20 | 'struct', 21 | 'common', 'partition', 'active', 22 | 'asm', 'class', 'union', 'enum', 'typedef', 'template', 23 | 'this', 'packed', 24 | 'goto', 'inline', 'noinline', 'volatile', 'public', 'static', 25 | 'extern', 'external', 'interface', 26 | 'long', 'short', 'double', 'half', 'fixed', 'unsigned', 'superp', 27 | 'input', 'output', 28 | 'filter', 29 | 'sizeof', 'cast', 30 | 'namespace', 'using' 31 | ); 32 | 33 | /* GLSL Built-In Functions */ 34 | restrictedFunctions.push( 35 | 'radians', 'degrees', 36 | 'sin', 'cos', 'tan', 37 | 'asin', 'acos', 'atan', 38 | 'sinh', 'cosh', 'tanh', 39 | 'asinh', 'acosh', 'atanh', 40 | 'pow', 'exp', 'log', 'exp2', 'log2', 41 | 'sqrt', 'inversesqrt', 42 | 'abs', 'sign', 'floor', 'trunc', 'round', 43 | 'ceil', 'fract', 44 | 'mod', 'modf', 'min', 'max', 45 | 'clamp', 'mix', 'step', 'smoothstep', 46 | 'isnan', 'isinf', 47 | 'length', 'distance', 'dot', 48 | 'cross', 'normalize', 'ftransform', 49 | 'faceforward', 50 | 'reflect', 'refract', 51 | 'transpose', 'inverse', 52 | 'equal', 'any', 'all', 'not', 53 | 'texture' 54 | ); 55 | 56 | const functionList = new Set(restrictedFunctions); 57 | export default functionList; 58 | -------------------------------------------------------------------------------- /src/gl-code/grammar.ne: -------------------------------------------------------------------------------- 1 | @builtin "whitespace.ne" 2 | @builtin "number.ne" 3 | 4 | # Defined this way for correct associativity + precedence 5 | 6 | sum -> 7 | sum _ sumOperator _ product {% 8 | (data) => [data[2], data[0], data[4]] 9 | %} 10 | | product {% id %} 11 | 12 | product -> 13 | product _ productOperator _ power {% 14 | (data) => [data[2], data[0], data[4]] 15 | %} 16 | | "-" _ power {% data => ['neg', data[2]] %} 17 | | power {% id %} 18 | 19 | power -> 20 | function _ powerOperator _ power {% 21 | (data) => ['pow', data[0], data[4]] 22 | %} 23 | | function {% id %} 24 | 25 | function -> 26 | unaryFunction "(" _ sum _ ")" {% data => [data[0][0], data[3]] %} 27 | | unaryFunction "[" _ sum _ "]" {% data => [data[0][0], data[3]] %} 28 | | binaryFunction "(" _ sum "," _ sum _ ")" {% data => [data[0][0], data[3], data[6]] %} 29 | | binaryFunction "[" _ sum "," _ sum _ "]" {% data => [data[0][0], data[3], data[6]] %} 30 | | fourFunction "(" _ sum "," _ sum "," _ sum "," _ sum _ ")" {% data => [data[0][0], data[3], data[6], data[9], data[12]] %} 31 | | fourFunction "[" _ sum "," _ sum "," _ sum "," _ sum _ "]" {% data => [data[0][0], data[3], data[6], data[9], data[12]] %} 32 | | diffFunction "(" _ sum _ ")" {% data => [data[0][0], data[3], ['variable', 'z']] %} 33 | | diffFunction "[" _ sum _ "]" {% data => [data[0][0], data[3], ['variable', 'z']] %} 34 | | parenthesis2 {% id %} 35 | 36 | parenthesis -> 37 | "(" sum ")" {% (data) => data[1] %} 38 | | "[" sum "]" {% (data) => data[1] %} 39 | | literal {% id %} 40 | 41 | parenthesis2 -> 42 | parenthesis {% id %} 43 | | parenthesis "!" {% (data) => ['factorial', data[0]] %} 44 | 45 | 46 | ##### Operators ##### 47 | sumOperator -> 48 | "+" {% () => 'add' %} 49 | | "-" {% () => 'sub' %} 50 | | "−" {% () => 'sub' %} 51 | 52 | productOperator -> 53 | "*" {% () => 'mul' %} 54 | | "×" {% () => 'mul' %} 55 | | "/" {% () => 'div' %} 56 | | "%" {% () => 'mod' %} 57 | 58 | powerOperator -> "**" | "^" 59 | 60 | ##### Functions ##### 61 | fourFunction -> 62 | "sum" 63 | | "product" {% () => ['prod'] %} 64 | | "prod" 65 | 66 | binaryFunction -> 67 | "beta" 68 | | "binom" 69 | | "binomial" {% () => ['binom'] %} 70 | | "choose" {% () => ['binom'] %} 71 | | "sn" 72 | | "cn" 73 | | "dn" 74 | | "wp" 75 | | "wp'" {% () => ['wpp'] %} 76 | | "theta00" 77 | | "theta01" 78 | | "theta10" 79 | | "theta11" 80 | | diffFunction {% x => x[0] %} 81 | 82 | diffFunction -> 83 | "derivative" {% () => ['diff'] %} 84 | | "diff" 85 | 86 | unaryFunction -> 87 | trigFunction 88 | | "cis" 89 | | "exp" 90 | | "log" 91 | | "ln" {% () => ['log'] %} 92 | | "sqrt" 93 | | "√" {% () => ['sqrt'] %} 94 | | "gamma" 95 | | "eta" 96 | | "zeta" 97 | | "erf" 98 | | "abs" 99 | | "arg" 100 | | "sgn" 101 | | "conj" 102 | | "real" 103 | | "imag" 104 | | "floor" 105 | | "ceil" 106 | | "round" 107 | | "step" 108 | | "re" {% () => ['real'] %} 109 | | "im" {% () => ['imag'] %} 110 | | "nome" 111 | | "sm" 112 | | "cm" 113 | | "j" 114 | | "e4" | "e6" | "e8" | "e10" | "e12" | "e14" | "e16" 115 | | "lambertw" 116 | 117 | # Trigonometric functions 118 | baseTrigFunction -> 119 | "sin" | "cos" | "tan" | "sec" | "csc" | "cot" 120 | 121 | hyperbolicTrigFunction -> 122 | baseTrigFunction "h" {% (data) => data.join('') %} 123 | 124 | trigFunction -> 125 | "arc":? baseTrigFunction {% (data) => data.join('') %} 126 | | "a" baseTrigFunction {% (data) => 'arc' + data[1] %} 127 | | "ar":? hyperbolicTrigFunction {% (data) => data.join('') %} 128 | 129 | ##### Literals ##### 130 | literal -> 131 | complexNumber {% id %} 132 | | variable {% id %} 133 | 134 | 135 | variable -> [a-z]:+ {% 136 | function(data, l, reject) { 137 | const constants = ['e', 'pi', 'tau', 'phi']; 138 | const token = data[0].join('') 139 | if (token === 'i') {return reject;} 140 | return constants.includes(token) ? ['constant', token] : ['variable', token]; 141 | } 142 | %} 143 | 144 | complexNumber -> 145 | decimal {% (data) => ['number', data[0], 0] %} 146 | | decimal "i" {% (data) => ['number', 0, data[0]] %} 147 | | "i" {% () => ['number', 0, 1] %} 148 | -------------------------------------------------------------------------------- /src/gl-code/make: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | npx nearleyc grammar.ne -o grammar.js 3 | npx nearley-railroad grammar.ne -o grammar.html 4 | -------------------------------------------------------------------------------- /src/gl-code/scene.js: -------------------------------------------------------------------------------- 1 | import createShaderProgram from './shaders.js'; 2 | 3 | 4 | function initBuffers(gl) { 5 | const vertexBuffer = gl.createBuffer(); 6 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 7 | 8 | // Positions of vertices (two triangles) 9 | const vertices = [ 10 | -1, -1, 11 | 1, -1, 12 | -1, 1, 13 | 14 | -1, 1, 15 | 1, -1, 16 | 1, 1 17 | ] 18 | 19 | // Initialize buffer with position data 20 | gl.bufferData( 21 | gl.ARRAY_BUFFER, 22 | new Float32Array(vertices), 23 | gl.STATIC_DRAW 24 | ); 25 | } 26 | 27 | function initializeScene(gl, expression, customShader, variableNames) { 28 | if (expression === null) {return null;} 29 | 30 | const shaderProgram = createShaderProgram( 31 | gl, 32 | expression, customShader, 33 | variableNames 34 | ); 35 | 36 | if (shaderProgram === null) { 37 | console.error('AST could not be compiled:', expression); 38 | return null; 39 | } 40 | 41 | // Initialize shader program 42 | initBuffers(gl); 43 | gl.useProgram(shaderProgram); 44 | 45 | // Initialize vertex array 46 | const positionLocation = gl.getAttribLocation(shaderProgram, 'a_position'); 47 | gl.enableVertexAttribArray(positionLocation); 48 | gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); 49 | 50 | // Retrieve pointers to variables 51 | const variableLocations = {}; 52 | for (const name of variableNames) { 53 | variableLocations[name] = gl.getUniformLocation(shaderProgram, name); 54 | } 55 | return variableLocations; 56 | } 57 | 58 | function drawScene(gl, variables, axis_ctx) { 59 | // Set variable values 60 | for (const key of Object.keys(variables)) { 61 | const [location, value] = variables[key]; 62 | if (gl.LOG_MODE) { 63 | gl.uniform3f(location, value, 0, 0); 64 | } else { 65 | gl.uniform2f(location, value, 0); 66 | } 67 | } 68 | 69 | // Draw scene 70 | gl.drawArrays(gl.TRIANGLES, 0, 6); 71 | 72 | // Draw coordinate axes 73 | drawAxes(axis_ctx, variables); 74 | } 75 | 76 | function drawAxes(ctx, variables) { 77 | // Clear canvas 78 | const dpr = window.devicePixelRatio; 79 | const [width, height] = [window.innerWidth * dpr, window.innerHeight * dpr]; 80 | ctx.clearRect(0, 0, width, height); 81 | if (variables.enable_axes[1] < 0.5) {return;} 82 | 83 | // Compute display scales 84 | const scale = Math.exp(variables.log_scale[1]) * dpr; 85 | 86 | let rawLogLabelScale = 2.3 - variables.log_scale[1] / Math.log(10); 87 | rawLogLabelScale += 3e-2 * Math.abs(rawLogLabelScale); // Make room for long labels 88 | let logLabelScale = Math.round(rawLogLabelScale); 89 | let labelScale = Math.pow(10, logLabelScale); 90 | 91 | if (logLabelScale - rawLogLabelScale > 0.2) { 92 | labelScale /= 5; 93 | logLabelScale--; 94 | } else if (logLabelScale - rawLogLabelScale > 0) { 95 | labelScale /= 2; 96 | logLabelScale--; 97 | } 98 | 99 | // Compute origin location in screen space 100 | const [x0, y0] = [ 101 | width/2 - scale*variables.center_x[1], 102 | height/2 + scale*variables.center_y[1], 103 | ]; 104 | 105 | // Compute window bounds 106 | const [x_min, x_max] = [-x0/scale, (width - x0)/scale]; 107 | const [y_min, y_max] = [(y0-height)/scale, y0/scale]; 108 | 109 | 110 | // Utility functions 111 | function horizontalLine(y) { 112 | const yy = Math.round(y0 - scale*y); 113 | ctx.moveTo(0, yy); 114 | ctx.lineTo(width, yy); 115 | } 116 | 117 | function verticalLine(x, lineWidth) { 118 | const xx = Math.round(x0 + scale*x); 119 | ctx.moveTo(xx, 0); 120 | ctx.lineTo(xx, height); 121 | } 122 | 123 | function xLabel(x) { 124 | const xx = x0 + scale * x; 125 | if (xx > width - 30*dpr || xx < 30*dpr) {return;} 126 | 127 | const dy = (y0 < height/3) ? 22 : -10; 128 | const y = Math.min(Math.max(y0 + dy*dpr, 90 * dpr), height-20*dpr); 129 | 130 | let label = x.toFixed(Math.max(0, -logLabelScale)).replace('-', '−'); 131 | const textWidth = ctx.measureText(label).width + 6 * dpr; 132 | 133 | ctx.textAlign = 'center' 134 | ctx.clearRect(xx - textWidth/2, y - 18*dpr, textWidth, 24*dpr); 135 | ctx.strokeText(label, xx, y); 136 | ctx.fillText(label, xx, y); 137 | } 138 | 139 | function yLabel(y, iWidth) { 140 | const yy = y0 - scale * y + 6 * dpr; 141 | if (yy > height - 50*dpr || yy < 100*dpr) {return;} 142 | 143 | const alignLeft = (x0 < 2*width/3); 144 | const dx = alignLeft ? 10: -10; 145 | ctx.textAlign = alignLeft ? 'left' : 'right'; 146 | 147 | const x = Math.min(Math.max(x0 + dx*dpr, 20 * dpr), width -20*dpr); 148 | 149 | let label = y.toFixed(Math.max(0, -logLabelScale)).replace('-', '−'); 150 | if (label === '1') {label = '';} 151 | if (label === '−1') {label = '−';} 152 | 153 | ctx.font = `${20 * dpr}px Computer Modern Serif`; 154 | const textWidth = ctx.measureText(label).width; 155 | 156 | const clearWidth = textWidth + iWidth + 8*dpr; 157 | ctx.clearRect( 158 | x - (alignLeft ? 3*dpr : clearWidth - 4*dpr), 159 | yy - 18*dpr, 160 | clearWidth, 161 | 24*dpr 162 | ); 163 | 164 | const textOffset = alignLeft ? 0 : -iWidth - dpr; 165 | ctx.strokeText(label, x + textOffset, yy); 166 | ctx.fillText(label, x + textOffset, yy); 167 | 168 | ctx.font = `italic ${20 * dpr}px Computer Modern Serif`; 169 | const iOffset = alignLeft ? textWidth + dpr : 0; 170 | ctx.strokeText('i', x + iOffset, yy); 171 | ctx.fillText('i', x + iOffset, yy); 172 | } 173 | 174 | 175 | // Draw gridlines 176 | ctx.globalAlpha = 0.8; 177 | ctx.strokeStyle= '#ffffff'; 178 | 179 | ctx.lineWidth = 1; 180 | ctx.beginPath(); 181 | for (let i = Math.ceil(x_min/labelScale); i < x_max/labelScale; i++) { 182 | if (i === 0) {continue;} 183 | verticalLine(i * labelScale); 184 | } 185 | for (let i = Math.ceil(y_min/labelScale); i < y_max/labelScale; i++) { 186 | if (i === 0) {continue;} 187 | horizontalLine(i * labelScale); 188 | } 189 | ctx.stroke(); 190 | 191 | ctx.lineWidth = 2; 192 | ctx.beginPath(); 193 | verticalLine(0); 194 | horizontalLine(0); 195 | ctx.stroke(); 196 | 197 | // Draw labels 198 | ctx.font = `${20 * dpr}px Computer Modern Serif`; 199 | ctx.globalAlpha = 1; 200 | ctx.fillStyle = '#ffffff'; 201 | ctx.lineWidth = 4; 202 | ctx.lineJoin = 'round'; 203 | ctx.strokeStyle = '#444444'; 204 | for (let i = Math.ceil(x_min/labelScale); i < x_max/labelScale; i++) { 205 | if (i === 0) {continue;} 206 | xLabel(i * labelScale); 207 | } 208 | 209 | 210 | ctx.font = `italic ${20 * dpr}px Computer Modern Serif`; 211 | const iWidth = ctx.measureText('i').width; 212 | for (let i = Math.ceil(y_min/labelScale); i < y_max/labelScale; i++) { 213 | if (i === 0) {continue;} 214 | yLabel(i * labelScale, iWidth); 215 | } 216 | } 217 | 218 | export {initializeScene, drawScene}; 219 | -------------------------------------------------------------------------------- /src/gl-code/scratch/erf.py: -------------------------------------------------------------------------------- 1 | k = np.arange(4) 2 | kkp = k*k/4.; 3 | 4 | print('vec4 kz = z.y * k;') 5 | print('vec4 kk = kkp + z.x*z.x + offset'); 6 | print('vec4 e1 = exp(kz - kk);') 7 | print('vec4 e2 = exp(-kz - kk);') 8 | print('series += 1./') 9 | -------------------------------------------------------------------------------- /src/gl-code/scratch/eta.py: -------------------------------------------------------------------------------- 1 | """Borwein series for Dirichlet eta.""" 2 | import math 3 | import numpy as np 4 | from scipy.special import factorial as fact 5 | 6 | n = 49 7 | 8 | d = [n * sum(fact(n+l-1) * 4**l / (fact(n-l) * fact(2*l)) for l in range(k+1)) for k in range(n+1)] 9 | 10 | coefficients = [((-1)**k * (d[n] - d[k]))/d[n] for k in range(n)] 11 | 12 | coefficients = np.array(coefficients)[1:] 13 | ns = np.arange(n+1)[2:] 14 | 15 | for i in [0, 16, 32]: 16 | print('mat4(' + ','.join(f'-{x:.14f}' for x in np.log(ns[i:i+16])) + '),') 17 | print('mat4(' + ','.join(f'{x:.14f}' for x in coefficients[i:i+16]) + '));') 18 | print() 19 | -------------------------------------------------------------------------------- /src/gl-code/scratch/modular.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | 4 | def f(z): 5 | # Maximize Im(gamma(z)) by minimizing cz + d. 6 | coeffs = np.array([1, 0, 0, 1]) 7 | a = np.array([z.real, z.imag]) 8 | b = np.array([1., 0]) 9 | for i in range(32): 10 | mu = round(np.dot(a, b) / np.dot(b, b)) 11 | a -= mu * b 12 | coeffs[:2] -= mu * coeffs[2:] 13 | print(a, b, coeffs) 14 | 15 | mu = round(np.dot(a, b) / np.dot(a, a)) 16 | b -= mu * a 17 | coeffs[2:] -= mu * coeffs[:2] 18 | 19 | print(a, b, coeffs) 20 | 21 | w1 = coeffs[0] * z + coeffs[1] 22 | w2 = coeffs[2] * z + coeffs[3] 23 | print(abs(w1), abs(w2)) 24 | return coeffs 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/gl-code/scratch/zeta.py: -------------------------------------------------------------------------------- 1 | """Borwein series for Dirichlet eta.""" 2 | import math 3 | import numpy as np 4 | 5 | ns = 2 + 16*1 + np.arange(16) 6 | print('mat4(' + ','.join(f'{x:.0f}.' for x in ns) + '),') 7 | print('mat4(' + ','.join(f'-{x:.12f}' for x in np.log(ns)) + ')') 8 | -------------------------------------------------------------------------------- /src/gl-code/shaders.js: -------------------------------------------------------------------------------- 1 | import { 2 | functionDefinitions, 3 | } from './complex-functions.js'; 4 | import toGLSL from './translators/to-glsl'; 5 | 6 | function loadShader(gl, type, source) { 7 | // Create and compile shader 8 | const shader = gl.createShader(type); 9 | gl.shaderSource(shader, source); 10 | gl.compileShader(shader); 11 | 12 | // Test for successful compilation 13 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 14 | console.error('Shader failed to compile: ' 15 | + gl.getShaderInfoLog(shader)); 16 | gl.deleteShader(shader); 17 | return null; 18 | } 19 | 20 | return shader; 21 | } 22 | 23 | 24 | function createShaderProgram(gl, expression, customShader, variableNames) { 25 | if (expression === null) {return null;} 26 | 27 | gl.LOG_MODE = !customShader; // Whether to use (log-magnitude, phase) representation 28 | const fragmentShaderSource = getFragmentShaderSource( 29 | expression, 30 | customShader, 31 | gl.drawingBufferWidth, gl.drawingBufferHeight, 32 | variableNames, 33 | gl.LOG_MODE 34 | ); 35 | 36 | if (fragmentShaderSource === null) {return null;} 37 | 38 | // console.log(fragmentShaderSource); 39 | 40 | // Load vertex and fragment shaders 41 | const vertexShader = loadShader(gl, 42 | gl.VERTEX_SHADER, vertexShaderSource); 43 | const fragmentShader = loadShader(gl, 44 | gl.FRAGMENT_SHADER, fragmentShaderSource); 45 | 46 | if (vertexShader === null | fragmentShader === null) { 47 | return null; 48 | } 49 | 50 | // TODO Debug 51 | // const ext = gl.getExtension('WEBGL_debug_shaders'); 52 | // console.log(ext.getTranslatedShaderSource(fragmentShader)); 53 | 54 | // Create shader program 55 | const shaderProgram = gl.createProgram(); 56 | gl.attachShader(shaderProgram, vertexShader); 57 | gl.attachShader(shaderProgram, fragmentShader); 58 | gl.linkProgram(shaderProgram); 59 | 60 | // Test for successful linkage 61 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 62 | console.error('Shader program failed to initialize: ' 63 | + gl.getProgramInfoLog(shaderProgram)); 64 | return null; 65 | } 66 | 67 | return shaderProgram; 68 | } 69 | 70 | export default createShaderProgram; 71 | 72 | 73 | const vertexShaderSource = ` 74 | attribute vec2 a_position; 75 | 76 | void main() { 77 | gl_Position = vec4(a_position, 0, 1); 78 | } 79 | `; 80 | 81 | 82 | function getFragmentShaderSource(expression, customShader, width, height, variableNames, LOG_MODE) { 83 | const x_offset = (width/2).toFixed(2); 84 | const y_offset = (height/2).toFixed(2); 85 | const dpr = window.devicePixelRatio.toFixed(4); 86 | 87 | const vectype = LOG_MODE ? 'vec3' : 'vec2'; 88 | 89 | const variableDeclarations = variableNames.map( 90 | (name) => `uniform ${vectype} ${name};` 91 | ).join('\n'); 92 | 93 | let custom_code = ''; 94 | let glsl_expression = null; 95 | if (customShader) { 96 | custom_code = expression; 97 | glsl_expression = 'mapping(z)'; 98 | } else { 99 | glsl_expression = toGLSL(expression, LOG_MODE)[0]; 100 | if (LOG_MODE) { 101 | glsl_expression = `upconvert(${glsl_expression})` 102 | } 103 | } 104 | if (glsl_expression === null) {return null;} 105 | 106 | console.log('Compiled AST:', expression); 107 | console.log(`Shader Code (${LOG_MODE ? 'log-cart' : 'cartesian'}):`, glsl_expression); 108 | 109 | return ` 110 | #ifdef GL_FRAGMENT_PRECISION_HIGH 111 | precision highp float; 112 | #else 113 | precision mediump float; 114 | #endif 115 | 116 | const float PI = 3.14159265358979323846264; 117 | const float TAU = 2.*PI; 118 | const float E = 2.718281845904523; 119 | const float LN2 = 0.69314718055994531; 120 | const float LN2_INV = 1.442695040889634; 121 | const float LNPI = 1.1447298858494001741434; 122 | const float PHI = 1.61803398874989484820459; 123 | const float SQ2 = 1.41421356237309504880169; 124 | 125 | const float checkerboard_scale = 0.25; 126 | 127 | const ${vectype} ZERO = ${LOG_MODE ? 'vec3(0)' : 'vec2(0)'}; 128 | const ${vectype} ONE = ${LOG_MODE ? 'vec3(1., 0, 0)' : 'vec2(1., 0)'}; 129 | const ${vectype} I = ${LOG_MODE ? 'vec3(0, 1., 0)' : 'vec2(0, 1.)'}; 130 | const ${vectype} C_PI = ${LOG_MODE ? 'vec3(PI, 0, 0)' : 'vec2(PI, 0)'}; 131 | const ${vectype} C_TAU = ${LOG_MODE ? 'vec3(TAU, 0, 0)' : 'vec2(TAU, 0)'}; 132 | const ${vectype} C_E = ${LOG_MODE ? 'vec3(E, 0, 0)' : 'vec2(E, 0)'}; 133 | const ${vectype} C_PHI = ${LOG_MODE ? 'vec3(PHI, 0, 0)' : 'vec2(PHI, 0)'}; 134 | 135 | ${variableDeclarations} 136 | 137 | vec2 clogcart(${vectype} z) {return vec2(${LOG_MODE ? 'log(length(z.xy)) + z.z' : 'log(length(z))'}, atan(z.y, z.x+1e-20));} 138 | vec2 encodereal(float a) {return vec2(log(abs(a)), 0.5*PI*(1. - sign(a)));} 139 | ${LOG_MODE ? 'vec3 downconvert(vec3 z) {return vec3(vec2(z.xy) * exp(z.z), 0);}' : 'vec2 downconvert (vec2 z) {return z;}'} 140 | vec3 upconvert(vec3 z) {float l = length(z.xy); return vec3(z.xy/l, z.z + log(l));} 141 | float ordinate(${vectype} z) {return ${LOG_MODE ? 'z.z' : '0.'};} 142 | 143 | ${functionDefinitions(expression, LOG_MODE)} 144 | 145 | vec3 hsv2rgb(vec3 c) { 146 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 147 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 148 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 149 | } 150 | 151 | vec3 get_color(${vectype} z_int, float derivative, float phase_derivative) { 152 | vec2 magphase = clogcart(z_int); 153 | magphase.x *= LN2_INV; 154 | magphase.y = mod(magphase.y, TAU); 155 | vec2 z = ${LOG_MODE ? 'downconvert(z_int).xy' : 'z_int'}; 156 | 157 | float color_value; 158 | float color_saturation = 1.0; 159 | 160 | ${LOG_MODE ? `float phase_decay_factor = 1./clamp(8. * phase_derivative, 1., 10000.0); 161 | color_saturation *= phase_decay_factor;` : ''} 162 | 163 | if (continuous_gradient.x > 0.5) { 164 | float color_lightness = 0.5 + atan(0.35 * magphase.x)/PI; 165 | color_saturation = 1.0; 166 | 167 | if (invert_gradient.x > 0.5) { 168 | color_lightness = 1.0 - color_lightness; 169 | } 170 | 171 | /* HSL to HSV conversion */ 172 | color_lightness *= 2.0; 173 | color_saturation *= 1.0 - abs(color_lightness - 1.0); 174 | color_value = (color_lightness + color_saturation) / 2.0; 175 | color_saturation /= color_value; 176 | } else { 177 | color_value = 0.5 * exp2(fract(magphase.x)); 178 | 179 | if (invert_gradient.x > 0.5) { 180 | color_value = 1.5 - color_value; 181 | } 182 | 183 | ${LOG_MODE ? 'color_value += (0.75 - color_value) * (1. - phase_decay_factor);' : ''} 184 | } 185 | 186 | if (enable_checkerboard.x > 0.5) { 187 | vec2 checkerboard_components = floor(2.0 * fract(z/checkerboard_scale)); 188 | float checkerboard = floor(2.0 * fract((checkerboard_components.x + checkerboard_components.y)/2.0)); 189 | 190 | // Anti-Moire 191 | float decay_factor = clamp(40. * derivative, 1., 10000.0) - 1.; 192 | checkerboard = 0.5 + (checkerboard - 0.5) / (1. + 3. * decay_factor); 193 | 194 | if (magphase.x > 15.) {checkerboard = 0.5;} 195 | 196 | color_value *= 0.8 + 0.2 * checkerboard; 197 | } 198 | 199 | vec3 hsv_color = vec3(magphase.y/TAU, color_saturation, color_value); 200 | return hsv2rgb(hsv_color); 201 | } 202 | 203 | ${custom_code} 204 | 205 | const vec2 screen_offset = vec2(${x_offset}, ${y_offset}); 206 | vec2 from_pixel(vec2 xy) { 207 | vec2 plot_center = vec2(center_x.x, center_y.x); 208 | float scale = exp(-log_scale.x) / ${dpr}; 209 | return scale * (xy - screen_offset) + plot_center; 210 | } 211 | 212 | ${vectype} internal_mapping(vec2 xy) { 213 | vec2 z_int = from_pixel(xy); 214 | ${LOG_MODE ? 'vec3 z = vec3(z_int, 0);' : 'vec2 z = z_int;'} 215 | return ${glsl_expression}; 216 | } 217 | 218 | void main() { 219 | // Set up for supersampling 220 | const vec2 A = vec2(0.125, 0.375); 221 | const vec2 B = vec2(0.375, -0.125); 222 | vec2 xy = gl_FragCoord.xy; 223 | 224 | // 4-Rook supersampling 225 | ${vectype} w1 = internal_mapping(xy + A); 226 | ${vectype} w2 = internal_mapping(xy - A); 227 | ${vectype} w3 = internal_mapping(xy + B); 228 | ${vectype} w4 = internal_mapping(xy - B); 229 | 230 | // Anti-Moire 231 | float phase_derivative = ${LOG_MODE ? '0.5*(length(w1-w2) + length(w3-w4))' : '0.'}; 232 | float derivative = ${LOG_MODE ? 'phase_derivative * exp(min(w1.z, 20.))' : '0.5 * (length(w1 - w2) + length(w3 - w4))'}; 233 | 234 | vec3 color1 = get_color(w1, derivative, phase_derivative); 235 | vec3 color2 = get_color(w2, derivative, phase_derivative); 236 | vec3 color3 = get_color(w3, derivative, phase_derivative); 237 | vec3 color4 = get_color(w4, derivative, phase_derivative); 238 | 239 | vec3 color = 0.25 * (color1 + color2 + color3 + color4); 240 | gl_FragColor = vec4(color, 1.0); 241 | } 242 | `; 243 | } 244 | -------------------------------------------------------------------------------- /src/gl-code/translators/compiler.js: -------------------------------------------------------------------------------- 1 | /***** 2 | * Compile higher-order constructs in AST, 3 | * and perform some AST optimizations. 4 | */ 5 | import {constants, fns} from './to-js.js'; 6 | import diff, {substitute} from './derivative.js'; 7 | 8 | const math = require('mathjs'); 9 | 10 | 11 | // Return AST where binary operation `op` is applied 12 | // between all given terms (AST). 13 | function compose(terms, op, op4, op8) { 14 | // Empty sum/product 15 | if (terms.length === 0) {return (op === 'sum') ? ['number', 0, 0] : ['number', 1, 0];} 16 | 17 | // Trivial sum/product 18 | if (terms.length === 1) {return terms[0]}; 19 | 20 | // Distribute evenly for faster computation 21 | const N = Math.floor(terms.length/2); 22 | const app = (a, b) => compose(terms.slice(a, b), op, op4, op8); 23 | 24 | if (N >= 4 && op8 !== undefined) { 25 | const NN = Math.floor(N/2); 26 | const NNN = Math.floor(N/4); 27 | return compile([op8, 28 | app(0, NNN), app(NNN, NN), app(NN, NN+NNN), app(NN+NNN, N), 29 | app(N, N+NNN), app(N+NNN, N+NN), app(N+NN, N+NN+NNN), app(N+NN+NNN, undefined) 30 | ]); 31 | } else { 32 | if (N >= 2 && op4 !== undefined) { 33 | const NN = Math.floor(N/2); 34 | return compile([op4, app(0, NN), app(NN, N), app(N, N+NN), app(N+NN, undefined)]); 35 | } else { 36 | return compile([op, compose(terms.slice(0, N), op), compose(terms.slice(N), op)]); 37 | } 38 | } 39 | } 40 | 41 | // Apply sum or product operator. 42 | function sumProd(operator, args) { 43 | // Evaluate lower/upper bounds 44 | args[2] = compile(args[2]); 45 | args[3] = compile(args[3]); 46 | 47 | if (args[1][0] !== 'variable') {return null;} 48 | if (args[2][0] !== 'number') {return null;} 49 | if (args[3][0] !== 'number') {return null;} 50 | 51 | const idxVar = args[1][1]; 52 | const low = args[2][1]; 53 | const high = args[3][1]; 54 | const termAST = args[0]; 55 | 56 | const terms = []; 57 | for (let i = low; i <= high; i++) { 58 | terms.push(compile(substitute(termAST, idxVar, ['number', i, 0]))); 59 | } 60 | 61 | if (operator === 'sum') {return compose(terms, 'add', 'add4', 'add8');} // Log-cartesian 62 | // if (operator === 'sum') {return compose(terms, 'add');} // Cartesian 63 | if (operator === 'prod') {return compose(terms, 'mul', 'mul4');} 64 | } 65 | 66 | function getConst(val) { 67 | let re = null; 68 | let im = null; 69 | 70 | if (!isNaN(val)) {re = val; im = 0;} 71 | if (val[0] === 'number') {re = val[1]; im = val[2];} 72 | if (val[0] === 'constant') {re = constants[val[1]]; im = 0;} 73 | 74 | return math.complex(re, im); 75 | } 76 | 77 | function destructure(val) { 78 | if (val.re === undefined) {return ['number', val, 0];} 79 | return ['number', val.re, val.im]; 80 | } 81 | 82 | function isConst(ast) { 83 | return !isNaN(ast) || ast[0] === 'number' || ast[0] === 'constant'; 84 | } 85 | 86 | function isZero(ast) { 87 | return ast[0] === 'number' && ast[1] === 0 && ast[2] === 0; 88 | } 89 | 90 | const inverseMap = { 91 | 'neg': 'neg', 92 | 'reciprocal': 'reciprocal', 93 | 'conj': 'conj', 94 | 'exp': 'log', 95 | 'sin': 'arcsin', 96 | 'cos': 'arccos', 97 | 'tan': 'arctan', 98 | 'sec': 'arcsec', 99 | 'csc': 'arccsc', 100 | 'cot': 'arccot', 101 | 'sinh': 'arsinh', 102 | 'cosh': 'arcosh', 103 | 'tanh': 'artanh', 104 | 'sech': 'arsech', 105 | 'csch': 'arcsch', 106 | 'coth': 'arcoth', 107 | 'square': 'sqrt', 108 | } 109 | 110 | 111 | // Optimize AST, and expand any higher-order constructs. 112 | function compile(ast) { 113 | if (!Array.isArray(ast)) {return ast;} 114 | 115 | let [operator, ...args] = ast; 116 | if (operator === 'number' || operator === 'variable' || operator === 'constant') { 117 | return ast; 118 | } 119 | 120 | // Higher-order functions 121 | if (operator === 'sum' || operator === 'prod') { 122 | return sumProd(operator, args); 123 | } 124 | 125 | args = args.map(compile); 126 | if (operator === 'diff') { 127 | return diff(args[0], args[1], compile); 128 | } 129 | 130 | // Aliases 131 | if (operator === 'factorial') {return compile(['gamma', ['add', args[0], ['number', 1, 0]]]);} 132 | 133 | 134 | // Evaluate if all arguments are constant 135 | if (args.every(isConst)) { 136 | const fn = fns[operator] || math[operator]; 137 | return destructure(fn(...args.map(getConst))); 138 | } 139 | 140 | // Cancel out inverse functions 141 | if (inverseMap[operator] !== undefined) { 142 | if (Array.isArray(args[0]) && args[0][0] === inverseMap[operator]) { 143 | return args[0][1]; 144 | } 145 | } 146 | 147 | 148 | // Optimizations 149 | if (operator === 'add') { 150 | if (isZero(args[0])) {return args[1];} 151 | if (isZero(args[1])) {return args[0];} 152 | } 153 | 154 | if (operator === 'sub') { 155 | if (isZero(args[0])) {return compile(['neg', args[1]]);} 156 | if (isZero(args[1])) {return args[0];} 157 | } 158 | 159 | if (operator === 'div') { 160 | if (isConst(args[1])) { 161 | return compile(['mul', compile(['reciprocal', args[1]]), args[0]]); 162 | } 163 | 164 | if (isConst(args[0])) { 165 | return compile(['mul', args[0], compile(['reciprocal', args[1]])]); 166 | } 167 | } 168 | 169 | if (operator === 'mul') { 170 | // Place constant in front 171 | if (isConst(args[1])) {args = [args[1], args[0]];} 172 | 173 | // Deal with constant case 174 | if (isConst(args[0])) { 175 | const val = getConst(args[0]); 176 | 177 | // Real scale factor 178 | if (val.im === 0) { 179 | return compile(['component_mul', args[1], val.re]); 180 | } 181 | } 182 | } 183 | 184 | if (operator === 'component_mul') { 185 | if (args[1] === 0) {return ['number', 0, 0];} 186 | if (args[1] === 1) {return args[0];} 187 | if (args[1] === -1) {return ['neg', args[0]];} 188 | if (args[1] > 0) { 189 | return ['component_mul_prelog', args[0], math.log(args[1])]; 190 | } else { 191 | return ['component_mul_prelog', compile(['neg', args[0]]), math.log(-args[1])]; 192 | } 193 | } 194 | 195 | if (operator === 'pow') { 196 | if (isConst(args[0])) { 197 | return ['exp', compile(['mul', compile(['log', args[0]]), args[1]])]; 198 | } 199 | 200 | if (isConst(args[1])) { 201 | const val = getConst(args[1]); 202 | const subAST = args[0]; 203 | if (val.im === 0) { 204 | if (val.re === -1) {return compile(['reciprocal', subAST]);} 205 | if (val.re === 0) {return ['number', 1, 0];} 206 | if (val.re === 0.5) {return ['sqrt', subAST];} 207 | if (val.re === 1) {return subAST;} 208 | if (val.re === 2) {return compile(['square', subAST]);} 209 | return ['exp', compile(['component_mul', ['log', subAST], val.re])]; // Cartesian only 210 | } 211 | } 212 | 213 | // return ['exp', ['mul', ['log', args[0]], args[1]]]; // Cartesian only 214 | } 215 | 216 | if (operator === 'beta') { 217 | if (isConst(args[0])) {args = [args[1], args[0]];} 218 | if (isConst(args[1])) { 219 | const val = getConst(args[1]); 220 | if (val.im === 0 && Number.isInteger(val.re) && val.re > 0 && val.re < 20) { 221 | const prefactor = compile(['component_mul', args[0], math.factorial(val.re)/val.re]); 222 | const terms = [prefactor]; 223 | for (let i = 1; i < val.re; i++) { 224 | terms.push(['add', args[0], ['number', i, 0]]); 225 | } 226 | return ['reciprocal', compose(terms, 'mul', 'mul4')]; 227 | } 228 | } 229 | } 230 | 231 | if (operator === 'binom') { 232 | if (isConst(args[1])) { 233 | const val = getConst(args[1]); 234 | if (val.im === 0) { 235 | if (val.re === 0) {return ['number', 1, 0];} 236 | if (Number.isInteger(val.re) && val.re > 0 && val.re < 20) { 237 | const terms = []; 238 | for (let i = 0; i < val.re; i++) { 239 | terms.push(['sub', args[0], ['number', i, 0]]); 240 | } 241 | return compile(['component_mul', compose(terms, 'mul', 'mul4'), 1/math.factorial(val.re)]); 242 | } 243 | } 244 | } 245 | if (isZero(args[0])) {return ['number', 0, 0];} 246 | } 247 | 248 | return [operator, ...args]; 249 | } 250 | 251 | export default compile; 252 | -------------------------------------------------------------------------------- /src/gl-code/translators/custom-functions.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const I = math.complex(0, 1); 4 | const isZero = (x) => (x === 0 || (x.re === 0 && x.im === 0)); 5 | 6 | const cadd = math.sum; 7 | const csub = math.subtract; 8 | const cdiv = math.divide; 9 | const cmul = math.multiply; 10 | const cmul_i = z => math.multiply(z, I); 11 | const creciprocal = z => math.divide(1, z); 12 | const csin = math.sin; 13 | const cexp = math.exp; 14 | const clog = math.log; 15 | const csqrt = math.sqrt; 16 | const cpow = math.pow; 17 | const csquare = z => cmul(z, z); 18 | const ccis = z => cexp(cmul_i(z)); 19 | 20 | const gamma_right = math.gamma; 21 | const gamma_left = z => math.divide(math.pi, math.multiply( 22 | math.sin(math.multiply(z, math.pi)), 23 | gamma_right(math.subtract(1, z)) 24 | )); 25 | const gamma = z => z.re < 0.5 ? gamma_left(z) : gamma_right(z); 26 | 27 | const beta = (z, w) => cdiv(cmul(gamma(z), gamma(w)), gamma(cadd(z, w))); 28 | const binom = (z, w) => (isZero(w) || isZero(csub(z, w))) ? 1 : (isZero(z) ? 0 : cdiv(z, cmul(cmul(w, csub(z, w)), beta(w, csub(z, w))))); 29 | const nome = z => cmul(cmul_i(clog(z)), -0.5/Math.PI); 30 | 31 | 32 | const ETA_COEFFICIENTS = [ 33 | [ 1.00000000000000000000, 0.00000000000000000000], 34 | [-1.00000000000000000000, 0.69314718055994528623], 35 | [ 1.00000000000000000000, 1.09861228866810978211], 36 | [-1.00000000000000000000, 1.38629436111989057245], 37 | [ 0.99999999999999555911, 1.60943791243410028180], 38 | [-0.99999999999979938270, 1.79175946922805495731], 39 | [ 0.99999999999386091076, 1.94591014905531323187], 40 | [-0.99999999986491050485, 2.07944154167983574766], 41 | [ 0.99999999776946757457, 2.19722457733621956422], 42 | [-0.99999997147371189055, 2.30258509299404590109], 43 | [ 0.99999971045373836631, 2.39789527279837066942], 44 | [-0.99999762229395061652, 2.48490664978800035456], 45 | [ 0.99998395846577381452, 2.56494935746153673861], 46 | [-0.99990996358087780305, 2.63905732961525840707], 47 | [ 0.99957522481587246510, 2.70805020110221006391], 48 | [-0.99830090896564482872, 2.77258872223978114491], 49 | [ 0.99419535104495204703, 2.83321334405621616526], 50 | [-0.98295446518722640050, 2.89037175789616451738], 51 | [ 0.95672573151919981793, 2.94443897916644026225], 52 | [-0.90449212250748245445, 2.99573227355399085425], 53 | [ 0.81569498718756294764, 3.04452243772342301398], 54 | [-0.68698555062628596790, 3.09104245335831606667], 55 | [ 0.52834368695773525904, 3.13549421592914967505], 56 | [-0.36280435095576935023, 3.17805383034794575181], 57 | [ 0.21751716776255453079, 3.21887582486820056360], 58 | [-0.11124997091266028426, 3.25809653802148213586], 59 | [ 0.04729731398489586680, 3.29583686600432912428], 60 | [-0.01619245778522847637, 3.33220451017520380432], 61 | [ 0.00427566222821304867, 3.36729582998647414271], 62 | [-0.00081524972526846008, 3.40119738166215546116], 63 | [ 0.00009970680093076545, 3.43398720448514627179], 64 | [-0.00000586510593565794, 3.46573590279972654216], 65 | ]; 66 | 67 | function eta_right(z) { 68 | return math.sum(...ETA_COEFFICIENTS.map( 69 | ([a, b]) => cmul(a, cexp(cmul(-b, z))) 70 | )); 71 | } 72 | 73 | function eta_left(w) { 74 | const z = w.neg(); 75 | const zp1 = math.add(z, 1); 76 | 77 | if (z.im < 0) {return math.conj(eta_left(math.conj(w)));} 78 | 79 | 80 | let component_a = cmul(gamma(z), csin(cmul(z, math.pi/2))); 81 | if (z.im > 50) { 82 | const log_r = math.log(math.abs(z)); 83 | const theta = math.arg(z); 84 | 85 | component_a = cmul( 86 | Math.sqrt(2 * Math.PI) / 2, 87 | I, 88 | cexp(cadd( 89 | cmul(theta - Math.PI/2, I, z), 90 | cmul(log_r - 1, z), 91 | cmul(-0.5, math.complex(log_r, theta)) 92 | ) 93 | )); 94 | } 95 | const component_b = cmul(z, eta_right(zp1)); 96 | 97 | const multiplier_a = cexp(cmul(-math.log(math.pi), zp1)); 98 | const multiplier_b = cdiv( 99 | csub(1, cexp(cmul(-Math.LN2, zp1))), 100 | csub(1, cexp(cmul(-Math.LN2, z))) 101 | ); 102 | 103 | const component = cmul(component_a, component_b); 104 | const multiplier = cmul(multiplier_a, multiplier_b); 105 | 106 | return cmul(2, cmul(component, multiplier)); 107 | } 108 | 109 | const eta = z => z.re < 0 ? eta_left(z) : eta_right(z); 110 | const zeta = z => cdiv(eta(z), csub(1, cexp(cmul(Math.LN2, csub(1, z))))); 111 | 112 | // Small z: https://math.stackexchange.com/questions/712434/erfaib-error-function-separate-into-real -and-imaginary-part 113 | // Large z: Expansion around infinity. 114 | function erf_large(z) { 115 | const k = cmul_i(creciprocal(z)); 116 | const k2 = csquare(k); 117 | const corrections = cdiv( 118 | cmul(k, cadd(1, cmul(k2, cadd(0.5, cmul(0.75, k2))))), 119 | math.sqrt(Math.PI) 120 | ); 121 | return cadd(1, cmul_i(cmul(cexp(csquare(cmul_i(z))), corrections))); 122 | } 123 | 124 | function erf(z) { 125 | if (Math.abs(z.im) > 8.5) { 126 | return erf_large(z); 127 | } 128 | 129 | const K = math.exp(-z.re*z.re)/Math.PI; 130 | const q = 4*z.re*z.re; 131 | const a = math.cos(2*z.re*z.im); 132 | const b = math.sin(2*z.re*z.im); 133 | 134 | const series = [math.erf(z.re), cmul(K/(2*z.re), math.complex(1-a, b))]; 135 | for (let k = 1; k < 65; k++) { 136 | const kk = k*k/4 + z.re*z.re; 137 | const e1 = math.exp(k*z.im - kk)/2; 138 | const e2 = math.exp(-k*z.im - kk)/2; 139 | const multiplier = 1/(k*k+q) * 2/Math.PI; 140 | const re = multiplier * (2*z.re*(math.exp(-kk)-a*(e1+e2)) + k*b*(e1-e2)); 141 | const im = multiplier * (2*z.re*b*(e1+e2) + k*a*(e1-e2)); 142 | series.push(math.complex(re, im)); 143 | } 144 | 145 | return math.sum(series); 146 | } 147 | 148 | function lambertw(z) { 149 | const L1 = math.log(z); 150 | let est = math.subtract(L1, math.log(L1)); 151 | if (math.abs(z) < 3) { 152 | est = math.subtract(math.sqrt(math.add(1, math.multiply(Math.E, z))), 1); 153 | } 154 | for (let i = 0; i < 6; i++) { 155 | est = math.subtract(est, math.divide( 156 | math.multiply(est, math.subtract(math.add(est, math.log(est)), L1)), 157 | math.add(1, est) 158 | )); 159 | } 160 | return est; 161 | } 162 | 163 | /***** Elliptic Functions *****/ 164 | function theta00(z, tau) { 165 | let result = 1; 166 | for (let n = 1; n < 8; n++) { 167 | const A = math.complex(0, Math.PI * n); 168 | const B = cmul(A, 2, z); 169 | const C = cmul(A, n, tau); 170 | result = cadd( 171 | result, 172 | cexp(cadd(C, B)), 173 | cexp(csub(C, B)) 174 | ); 175 | } 176 | return result; 177 | } 178 | const theta01 = (z, tau) => theta00(cadd(z, 0.5), tau); 179 | const theta10 = (z, tau) => cmul( 180 | cexp(cmul(math.complex(0, Math.PI/4), cadd(tau, cmul(4, z)))), 181 | theta00(cadd(z, cmul(0.5, tau)), tau) 182 | ); 183 | const theta11 = (z, tau) => cmul( 184 | cexp(cmul(math.complex(0, Math.PI/4), cadd(tau, cmul(4, z), 2))), 185 | theta00(cadd(z, cmul(0.5, tau), 0.5), tau) 186 | ); 187 | 188 | function invert_tau(k) { 189 | const root_k = csqrt(csqrt(csub(1, cmul(k, k)))); 190 | const l = cmul(0.5, cdiv(csub(1, root_k), cadd(1, root_k))); 191 | const q = cadd( 192 | l, 193 | cmul(2, cpow(l, 5)), 194 | cmul(15, cpow(l, 9)), 195 | cmul(150, cpow(l, 13)) 196 | ); 197 | return cmul(clog(q), math.complex(0, -1/Math.PI)); 198 | } 199 | 200 | function jacobi_reduce(z, k) { 201 | const tau = invert_tau(k); 202 | const t00 = theta00(0, tau); 203 | const zz = cdiv(z, cmul(Math.PI, t00, t00)); 204 | const n = 2 * Math.round(0.5 * zz.im/tau.im); 205 | return [csub(zz, cmul(n, tau)), tau]; 206 | } 207 | 208 | function raw_sn(zz, tau) { 209 | return cdiv(cmul( 210 | -1, theta00(0, tau), theta11(zz, tau) 211 | ), cmul( 212 | theta10(0, tau), theta01(zz, tau) 213 | )); 214 | } 215 | 216 | function raw_cn(zz, tau) { 217 | return cdiv(cmul( 218 | theta01(0, tau), theta10(zz, tau) 219 | ), cmul( 220 | theta10(0, tau), theta01(zz, tau) 221 | )); 222 | } 223 | 224 | function raw_dn(zz, tau) { 225 | return cdiv(cmul( 226 | theta01(0, tau), theta00(zz, tau) 227 | ), cmul( 228 | theta00(0, tau), theta01(zz, tau) 229 | )); 230 | } 231 | 232 | const sn = (z, k) => raw_sn(...jacobi_reduce(z, k)); 233 | const cn = (z, k) => raw_cn(...jacobi_reduce(z, k)); 234 | const dn = (z, k) => raw_dn(...jacobi_reduce(z, k)); 235 | 236 | 237 | // Weierstrass p-function 238 | function wp(z, tau) { 239 | const n = Math.round(z.im/tau.im); 240 | const zz = csub(z, cmul(n, tau)); 241 | 242 | const t002 = csquare(theta00(0, tau)); 243 | const t102 = csquare(theta10(0, tau)); 244 | const e2 = cmul(-Math.PI*Math.PI/3, cadd(csquare(t102), csquare(t002))); 245 | return cadd(cmul( 246 | Math.PI*Math.PI, 247 | t002, t102, 248 | csquare(cdiv(theta01(zz, tau), theta11(zz, tau))) 249 | ), e2); 250 | } 251 | 252 | function raw_wpp(zz, A, tau) { 253 | return cmul( 254 | -2, 255 | cpow(cdiv(A, raw_sn(zz, tau)), 3), 256 | raw_cn(zz, tau), raw_dn(zz, tau) 257 | ); 258 | } 259 | 260 | function wpp(z, tau) { 261 | const t004 = csquare(csquare(theta00(0, tau))); 262 | const t104 = csquare(csquare(theta10(0, tau))); 263 | const t014 = csquare(csquare(theta01(0, tau))); 264 | 265 | const PI2_3 = Math.PI * Math.PI / 3; 266 | const e1 = cmul(PI2_3, cadd(t004, t014)); 267 | const e2 = cmul(-PI2_3, cadd(t104, t004)); 268 | const e3 = cmul(PI2_3, csub(t104, t014)); 269 | const A = csqrt(csub(e1, e3)); 270 | const B = csqrt(csub(e2, e3)); 271 | 272 | const u = cmul(z, A); 273 | const k = cdiv(B, A); 274 | const [zz, tau2] = jacobi_reduce(u, k); 275 | 276 | return raw_wpp(zz, A, tau2); 277 | } 278 | 279 | 280 | // Dixon elliptic functions 281 | const e2v = math.complex(0.20998684165, 0); 282 | const e1 = math.complex(-0.10499342083, 0.18185393933); 283 | const e3 = math.complex(-0.10499342083, -0.18185393933); 284 | 285 | const A = csqrt(csub(e1, e3)); 286 | const B = csqrt(csub(e2v, e3)); 287 | const k = cdiv(B, A); 288 | 289 | function cm(z) { 290 | const u = cmul(z, A); 291 | const [zz, tau] = jacobi_reduce(u, k); 292 | return cadd(1, cdiv(2, csub(cmul(3, raw_wpp(zz, A, tau)), 1))); 293 | } 294 | 295 | function sm(z) { 296 | return cm(csub(1.7666387502854499, z)); 297 | } 298 | 299 | function dot(z, w) {return z.re * w.re + z.im * w.im;} 300 | 301 | function lattice_reduce(z) { 302 | if (z.im < 0) {return [math.complex(0, 1/0), 0];} 303 | const coeffs = [math.complex(1, 0), I]; 304 | let a = z; 305 | let b = math.complex(1, 0); 306 | for (let i = 0; i < 16; i++) { 307 | let mu = Math.round(dot(a, b)/dot(b, b)); 308 | a = csub(a, cmul(mu, b)); 309 | coeffs[0] = csub(coeffs[0], cmul(mu, coeffs[1])); 310 | 311 | mu = Math.round(dot(a, b)/dot(a, a)); 312 | b = csub(b, cmul(mu, a)); 313 | coeffs[1] = csub(coeffs[1], cmul(mu, coeffs[0])); 314 | } 315 | const num = cadd(cmul(coeffs[0].re, z), coeffs[0].im); 316 | const denom = cadd(cmul(coeffs[1].re, z), coeffs[1].im); 317 | const res = cdiv(num, denom); 318 | if (math.abs(res) < 1) {return [cdiv(-1, res), num];} 319 | return [res, denom]; 320 | } 321 | 322 | function j(z) { 323 | z = lattice_reduce(z)[0]; 324 | const a = theta10(0, z); 325 | const b = theta00(0, z); 326 | const c = theta01(0, z); 327 | 328 | return cmul(32, cdiv( 329 | cpow(cadd(cadd(cpow(a, 8), cpow(b, 8)), cpow(c, 8)), 3), 330 | cpow(cmul(a, cmul(b, c)), 8) 331 | )); 332 | } 333 | 334 | function e_term(q, n, p) { 335 | const qn = math.pow(q, n); 336 | return cmul(math.pow(n, p), cdiv(qn, csub(1, qn))); 337 | } 338 | 339 | function eisenstein(z, coeff, pow) { 340 | let [zz, weight] = lattice_reduce(z); 341 | const q = ccis(cmul(2*Math.PI, zz)); 342 | let series = 0; 343 | for (let n = 1; n < 8; n++) {series = cadd(series, e_term(q, n, pow));} 344 | return cmul(cadd(1, cmul(coeff, series)), cpow(weight, -2*pow)); 345 | } 346 | 347 | function e2(z) {return eisenstein(z, -24, 1);} 348 | function e4(z) {return eisenstein(z, 240, 3);} 349 | function e6(z) {return eisenstein(z, -504, 5);} 350 | function e8(z) {return csquare(e4(z));} 351 | function e10(z) {return cmul(e4(z), e6(z));} 352 | function e12(z) {return cadd(cmul(441/691, cpow(e4(z), 3)), cmul(250/691, csquare(e6(z))));} 353 | function e14(z) {return cmul(e8(z), e6(z));} 354 | function e16(z) { 355 | const a = e4(z); const b = e6(z); 356 | return cadd(cmul(1617/3617, csquare(csquare(a))), cmul(2000/3617, cmul(a, csquare(b)))); 357 | } 358 | 359 | export { 360 | zeta, eta, gamma, beta, binom, erf, lambertw, 361 | nome, 362 | theta00, theta01, theta10, theta11, 363 | sn, cn, dn, 364 | wp, wpp, 365 | sm, cm, 366 | j, e2, e4, e6, e8, e10, e12, e14, e16 367 | }; 368 | -------------------------------------------------------------------------------- /src/gl-code/translators/derivative.js: -------------------------------------------------------------------------------- 1 | // Substitute bound variable with value in AST 2 | function substitute(ast, name, value) { 3 | if (!Array.isArray(ast)) {return ast;} 4 | if (ast[0] === 'variable' && ast[1] === name) {return value;} 5 | return ast.map(x => substitute(x, name, value)); 6 | } 7 | 8 | function contains(ast, name) { 9 | if (!Array.isArray(ast)) {return false;} 10 | if (ast[0] === 'variable' && ast[1] === name) {return true;} 11 | return ast.some(x => contains(x, name)); 12 | } 13 | 14 | const ZERO = ['number', 0, 0]; 15 | const ONE = ['number', 1, 0]; 16 | 17 | const diffTable = { 18 | 'sin': x => ['cos', x], 19 | 'cos': x => ['neg', ['sin', x]], 20 | 'tan': x => ['square', ['sec', x]], 21 | 'sec': x => ['mul', ['sec', x], ['tan', x]], 22 | 'csc': x => ['neg', ['mul', ['csc', x], ['cot', x]]], 23 | 'cot': x => ['neg', ['square', ['csc', x]]], 24 | 'arcsin': x => ['reciprocal', ['sqrt', ['sub', ONE, ['square', x]]]], 25 | 'arccos': x => ['neg', ['reciprocal', ['sqrt', ['sub', ONE, ['square', x]]]]], 26 | 'arctan': x => ['reciprocal', ['add', ONE, ['square', x]]], 27 | 'arcsec': x => ['reciprocal', ['mul', ['square', x], ['sqrt', ['sub', ONE, ['reciprocal', ['square', x]]]]]], 28 | 'arccsc': x => ['neg', ['reciprocal', ['mul', ['square', x], ['sqrt', ['sub', ONE, ['reciprocal', ['square', x]]]]]]], 29 | 'arccot': x => ['neg', ['reciprocal', ['add', ONE, ['square', x]]]], 30 | 31 | 'sinh': x => ['cosh', x], 32 | 'cosh': x => ['sinh', x], 33 | 'tanh': x => ['square', ['sech', x]], 34 | 'sech': x => ['neg', ['mul', ['tanh', x], ['sech', x]]], 35 | 'csch': x => ['neg', ['mul', ['coth', x], ['csch', x]]], 36 | 'coth': x => ['neg', ['square', ['csch', x]]], 37 | 'arsinh': x => ['reciprocal', ['sqrt', ['add', ['square', x], ONE]]], 38 | 'arcosh': x => ['reciprocal', ['mul', ['sqrt', ['sub', x, ONE]], ['sqrt', ['add', x, ONE]]]], 39 | 'artanh': x => ['reciprocal', ['sub', ONE, ['square', x]]], 40 | // 'arsech': x => ['neg', ['reciprocal', ['mul', ['mul', ['sqrt', ['add', ['reciprocal', x], ONE]], ['sqrt', ['sub', ['reciprocal', x], ONE]]], ['square', x]]]], 41 | 'arcsch': x => ['neg', ['reciprocal', ['mul', ['square', x], ['sqrt', ['add', ONE, ['reciprocal', ['square', x]]]]]]], 42 | 'arcoth': x => ['reciprocal', ['sub', ['square', x], ONE]], 43 | 44 | 'exp': x => ['exp', x], 45 | 'cis': x => ['mul_i', ['cis', x]], 46 | 'log': x => ['reciprocal', x], 47 | 48 | 'square': x => ['component_mul', x, 2], 49 | 'sqrt': x => ['reciprocal', ['component_mul', ['sqrt', x], 2]], 50 | 51 | 'erf': x => ['component_mul', ['exp', ['neg', ['square', x]]], 2/Math.sqrt(Math.PI)], 52 | 53 | 'lambertw': x => ['reciprocal', ['add', x, ['exp', ['lambertw', x]]]], 54 | } 55 | 56 | // Analytically compute the derivative of the given AST 57 | // with respect to variable `arg`. 58 | function diff(ast, arg, compile) { 59 | if (ast === null) {return null;} 60 | if (arg[0] !== 'variable') {return null;} 61 | 62 | if (!Array.isArray(ast)) {return ZERO;} 63 | if (ast[0] === 'constant' || ast[0] === 'number') {return ZERO;} 64 | if (ast[0] === 'variable') {return (ast[1] === arg[1]) ? ONE : ZERO;} 65 | 66 | if (!contains(ast, arg[1])) {return ZERO;} 67 | 68 | const [operator, ...args] = ast; 69 | 70 | // Sum rule 71 | if (operator === 'add' || operator === 'sub') { 72 | return compile([operator, ...args.map(x => diff(x, arg, compile))]); 73 | } 74 | 75 | // Product rule 76 | if (operator === 'mul') { 77 | return compile(['add', 78 | compile(['mul', args[0], diff(args[1], arg, compile)]), 79 | compile(['mul', args[1], diff(args[0], arg, compile)]), 80 | ]); 81 | } 82 | 83 | if (operator === 'component_mul') { 84 | return compile(['component_mul', diff(args[0], arg, compile), args[1]]); 85 | } 86 | 87 | // Quotient rule 88 | if (operator === 'div') { 89 | return compile(['div', 90 | compile(['sub', 91 | compile(['mul', args[1], diff(args[0], arg, compile)]), 92 | compile(['mul', args[0], diff(args[1], arg, compile)]) 93 | ]), 94 | ['square', compile(args[1])], 95 | ]); 96 | } 97 | 98 | // Chain rule (analytic derivative). 99 | const [analyticDiff, internal] = [diffTable[ast[0]], ast[1]]; 100 | if (analyticDiff !== undefined) { 101 | return compile(['mul', diff(internal, arg, compile), compile(analyticDiff(internal))]); 102 | } 103 | 104 | // Numerical fallback. 105 | console.log('numerical fallback:', ast, arg); 106 | return numericalDiff(ast, arg, compile); 107 | } 108 | 109 | function numericalDiff(ast, arg, compile) { 110 | const dz = 1e-2; // Finite difference step 111 | if (arg[0] !== 'variable') {return null;} 112 | 113 | const high = substitute(ast, arg[1], ['add', arg, ['number', dz, 0]]); 114 | const low = substitute(ast, arg[1], ['sub', arg, ['number', dz, 0]]); 115 | 116 | return compile(['component_mul', ['sub', high, low], 1/(2*dz)]); 117 | } 118 | 119 | export {substitute}; 120 | export default diff; 121 | -------------------------------------------------------------------------------- /src/gl-code/translators/to-glsl.js: -------------------------------------------------------------------------------- 1 | import {get} from 'lodash'; 2 | 3 | const math = require('mathjs'); 4 | 5 | function terminateFloat(x) { 6 | const terminator = Number.isInteger(x) ? '.' : ''; 7 | return x.toString() + terminator; 8 | } 9 | 10 | // Returns pair [ast_in_glsl, requires_parenthesis] 11 | function toGLSL(ast, LOG_MODE) { 12 | if (!isNaN(ast)) { 13 | // GLSL floats must end in decimal point 14 | return [terminateFloat(ast), false]; 15 | } 16 | if (!Array.isArray(ast)) {return [ast, false];} 17 | 18 | let infixOperators = { 19 | 'add': '+', 20 | 'sub': '-', 21 | 'component_mul': '*', 22 | }; 23 | if (LOG_MODE) { 24 | infixOperators = {}; 25 | } 26 | 27 | const [operator, ...args] = ast; 28 | 29 | if (operator === 'number') { 30 | const [real, imag] = args; 31 | if (LOG_MODE) { 32 | let length = math.hypot(real, imag) 33 | if (length === 0) {length = 1;} 34 | return [`vec3(${real/length}, ${imag/length}, ${math.log(length)})`, false]; 35 | } else { 36 | if (real === 1 && imag === 0) {return ['ONE', false];} 37 | if (real === 0 && imag === 1) {return ['I', false];} 38 | return [`vec2(${real}, ${imag})`, false]; 39 | } 40 | } 41 | 42 | if (operator === 'variable') {return [args[0], false];} 43 | if (operator === 'constant') {return ['C_' + args[0].toUpperCase(), false];} // TODO FIX 44 | if (operator in infixOperators) { 45 | const op = infixOperators[operator] 46 | let operands = args.map(x => toGLSL(x, LOG_MODE)); 47 | 48 | // Add parentheses where possibly necessary 49 | if (op === '-') { 50 | if (operands[1][1]) { 51 | operands[1][0] = '(' + operands[1][0] + ')'; 52 | }; 53 | } else { 54 | if (op !== '+') { 55 | operands = operands.map(x => [x[1] ? '(' + x[0] + ')' : x[0], false]); 56 | } 57 | } 58 | return [operands[0][0] + op + operands[1][0], operator !== 'mul']; 59 | } 60 | 61 | // Unary function 62 | const unaryFunctions = { 63 | 'factorial': 'cfact', 64 | }; 65 | const internalName = get(unaryFunctions, operator, 'c' + operator); 66 | 67 | return [internalName + '(' + args.map(x => toGLSL(x, LOG_MODE)[0]).join(', ') + ')', false]; 68 | } 69 | 70 | export default toGLSL; 71 | -------------------------------------------------------------------------------- /src/gl-code/translators/to-js.js: -------------------------------------------------------------------------------- 1 | import {get, isNil} from 'lodash'; 2 | import { 3 | zeta, eta, gamma, beta, binom, erf, lambertw, 4 | nome, 5 | theta00, theta01, theta10, theta11, 6 | sn, cn, dn, 7 | wp, wpp, 8 | sm, cm, 9 | j, e2, e4, e6, e8, e10, e12, e14, e16 10 | } from './custom-functions.js' 11 | const math = require('mathjs'); 12 | 13 | const constants = { 14 | 'e': Math.E, 15 | 'pi': Math.PI, 16 | 'tau': 2 * Math.PI, 17 | 'phi': (1 + Math.sqrt(5))/2, 18 | } 19 | 20 | function fract(z) {return math.complex(z.re - Math.floor(z.re), z.im - Math.floor(z.im));} 21 | const mod = (z, w) => math.multiply(w, fract(math.divide(z, w))); 22 | const add4 = (a, b, c, d) => math.add(math.add(a, b), math.add(c, d)); 23 | 24 | const I = math.complex(0, 1); 25 | const fns = { 26 | add8: (a, b, c, d, e, f, g, h) => math.add(add4(a, b, c, d), add4(e, f, g, h)), 27 | add4, 28 | mul4: (a, b, c, d) => math.multiply(math.multiply(a, b), math.multiply(c, d)), 29 | 30 | rawpow: math.pow, 31 | log: z => (z === 0) ? -1e100 : math.log((z.re < 0) ? math.add(z, math.complex(0, 1e-20)) : z), // Consistent branch cut 32 | 33 | sub: math.subtract, 34 | neg: math.unaryMinus, 35 | mul: math.multiply, 36 | div: math.divide, 37 | mod, 38 | reciprocal: z => math.divide(1, z), 39 | component_mul: (z, alpha) => math.complex(alpha*z.re, alpha*z.im), 40 | component_mul_prelog: (z, alpha) => math.complex(math.exp(alpha)*z.re, math.exp(alpha)*z.im), 41 | real: math.re, 42 | imag: math.im, 43 | step: z => (z.re >= 0) ? 1 : 0, 44 | 45 | arcsin: math.asin, 46 | arccos: math.acos, 47 | arctan: math.atan, 48 | arcsec: math.asec, 49 | arccsc: math.acsc, 50 | arccot: math.acot, 51 | 52 | arsinh: math.asinh, 53 | arcosh: math.acosh, 54 | artanh: math.atanh, 55 | arsech: math.asech, 56 | arcsch: math.acsch, 57 | arcoth: math.acoth, 58 | 59 | cis: z => math.exp(math.multiply(z, I)), 60 | 61 | gamma, beta, binom, 62 | eta, 63 | zeta, 64 | erf, 65 | lambertw, 66 | 67 | // factorial: z => gamma(math.add(z, 1)), 68 | 69 | nome, 70 | theta00, theta01, theta10, theta11, 71 | sn, cn, dn, 72 | wp, wpp, 73 | sm, cm, 74 | j, e2, e4, e6, e8, e10, e12, e14, e16, 75 | } 76 | 77 | /** 78 | * Returns a JS function that evaluates 79 | * the given AST. 80 | * 81 | * Inputs of the returned function 82 | * are [real, imag], 83 | * representing the complex input. 84 | * The returned function outputs a 85 | * 2-tuple [real, imag] 86 | * representing the complex output. 87 | */ 88 | function toJS(ast, variables) { 89 | const errorValue = [NaN, NaN]; 90 | if (ast === null) {return z => errorValue;} 91 | if (!isNaN(ast)) {return z => [ast, 0];} 92 | 93 | // Destructure this level of the AST 94 | const [operator, ...args] = ast; 95 | 96 | // Complex number literal 97 | if (operator === 'number') {return z => args;} 98 | 99 | // User-defined variable 100 | if (operator === 'variable') { 101 | const [name] = args; 102 | if (name === 'z') {return z => z;} 103 | return z => [get(variables, name, NaN), 0]; 104 | } 105 | 106 | // Built-in constant 107 | if (operator === 'constant') { 108 | const [name] = args; 109 | return z => [constants[name], 0]; 110 | } 111 | 112 | // Built-in function 113 | const func = fns[operator] || math[operator]; 114 | if (!isNil(func)) { 115 | const destructure = z => isNil(z.re) ? [z, 0] : [z.re, z.im]; 116 | return z => destructure(func(...args.map( 117 | subtree => math.complex(...toJS(subtree, variables)(z)) 118 | ))); 119 | } 120 | 121 | // Fallback if no match 122 | return z => errorValue; 123 | } 124 | 125 | export {constants, fns}; 126 | export default toJS; 127 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import {render} from 'react-snapshot'; 5 | 6 | import './index.css'; 7 | 8 | import App from './App'; 9 | import {unregister} from './registerServiceWorker'; 10 | 11 | 12 | render(, document.getElementById('root')); 13 | //registerServiceWorker(); 14 | unregister(); 15 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; 2 | import blue from '@material-ui/core/colors/blue'; 3 | 4 | const theme = createMuiTheme({ 5 | palette: { 6 | primary: blue, 7 | }, 8 | }); 9 | 10 | export default theme; 11 | --------------------------------------------------------------------------------