From c61598d14ee23b48cf4497fd184cc6dda0167c4f Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Sun, 23 Feb 2020 20:20:25 +0000
Subject: [PATCH 01/38] move touchBinds to generic touch bind
---
src/components/JuliaRenderer.jsx | 113 ++++---------------------
src/components/MandelbrotRenderer.jsx | 117 ++++----------------------
src/components/utils.js | 102 ++++++++++++++++++++++
3 files changed, 133 insertions(+), 199 deletions(-)
diff --git a/src/components/JuliaRenderer.jsx b/src/components/JuliaRenderer.jsx
index 9139b16..8543522 100644
--- a/src/components/JuliaRenderer.jsx
+++ b/src/components/JuliaRenderer.jsx
@@ -1,10 +1,10 @@
import React, { useRef, useEffect } from "react";
-import { useGesture, addV } from "react-use-gesture";
-import { scale } from "vec-la";
+import { useGesture } from "react-use-gesture";
import { animated } from "react-spring";
import newSmoothJuliaShader from "../shaders/newSmoothJuliaShader";
import _ from "lodash";
import WebGLCanvas from "./WebGLCanvas";
+import { genericTouchBind } from "./utils";
export default function JuliaRenderer(props) {
@@ -22,9 +22,9 @@ export default function JuliaRenderer(props) {
const screenScaleMultiplier = props.screenmult;
// read incoming props
- const [{ pos }, setControlPos] = props.controls.pos;
- const [{ theta, last_pointer_angle }, setControlRot] = props.controls.rot;
- const [{ zoom, last_pointer_dist, minZoom, maxZoom }, setControlZoom] = props.controls.zoom;
+ const [{ pos }] = props.controls.pos;
+ // const [{ theta, last_pointer_angle }, setControlRot] = props.controls.rot;
+ const [{ zoom }, setControlZoom] = props.controls.zoom;
const maxI = props.maxiter;
const AA = 1;
@@ -36,102 +36,19 @@ export default function JuliaRenderer(props) {
AA: 2,
});
- // the hook responsible for handling gestures
- const touchBind = useGesture({
-
- // prevent some browser events such as swipe-based navigation or
- // pinch-based zoom and instead redirect them to this handler
- onDragStart: ({ event }) => event.preventDefault(),
- onPinchStart: ({ event }) => event.preventDefault(),
-
- onPinch: ({ offset: [d], down, vdva, memo = [theta.getValue(), last_pointer_angle.getValue(), zoom.getValue(), last_pointer_dist.getValue()] }) => {
- // alert(mx, my)
- // let [theta, lpa] = memo
- let [,, z, lpd] = memo;
- // console.log(d);
- let d_rel = d / 250;
- let curr_zoom = zoom.getValue();
-
- setControlZoom({
- zoom: _.clamp(z + (d_rel - lpd) * Math.sign(curr_zoom) * Math.abs(curr_zoom ** 0.9), minZoom.getValue(), maxZoom.getValue()),
- last_pointer_dist: d_rel,
-
- immediate: down,
- // config: { velocity: vd, decay: true }
- });
-
- // setControlRot({
- // theta: t + (a - lpa),
- // last_pointer_angle: a,
-
- // immediate: down,
- // // config: { velocity: va, decay: true }
- // });
-
- return memo;
- },
-
- onPinchEnd: ({ vdva: [, va] }) => {
- setControlRot({
- // set theta so it's remembered next time
- theta: va,
-
- // config: { velocity: va, decay: true }
- });
- },
-
- onWheel: ({ movement: [, my], vxvy: [, vy] }) => {
- // x, y obtained from event
- let z = zoom.getValue();
- let newZ = z * (1 - my * (my < 0 ? 2e-3 : 9e-4));
-
- setControlZoom({
- zoom: _.clamp(newZ, minZoom.getValue(), maxZoom.getValue()),
- config: {
- // bias velocity towards zooming in (vy negative )
- // if zooming
- velocity: _.clamp(vy * (vy < 0 ? 2.5 : 1.5), -10, 10), // * z**0.9 - my/15,
- }
- });
- },
-
- onDrag: ({ down, movement, velocity, direction, memo = pos.getValue() }) => {
-
- // change according to this formula:
- // move (x, y) in the opposite direction of drag (pan with cursor)
- // divide by canvas size to scale appropriately
- // multiply by 2 to correct scaling on viewport
- // use screen multiplier for more granularity
- let realZoom = gl.current.canvas.height * zoom.getValue() * screenScaleMultiplier;
-
- let plotMovement = scale(movement, -2/realZoom);
-
- let relMove = [plotMovement[0], -plotMovement[1]];
- let relDir = [direction[0], -direction[1]];
-
- setControlPos({
- pos: addV(memo, relMove), // add the displacement to the starting position
- immediate: down, // immediately apply if the gesture is active
- config: {
- velocity: scale(relDir, -velocity/realZoom), // set the velocity (gesture momentum)
- decay: true,
- },
- });
-
- return memo;
- },
-
- }, {
- event: { passive: false, capture: false },
+ let gtb = genericTouchBind({
domTarget: canvasRef,
- // The config object passed to useGesture has drag, wheel, scroll, pinch and move keys
- // for specific gesture options. See here for more details.
- // drag: {
- // bounds,
- // rubberband: true,
- // }
+ posControl: props.controls.pos,
+ zoomControl: props.controls.zoom,
+ screenScaleMultiplier: screenScaleMultiplier,
+ gl: gl,
});
+ let touchBind = useGesture(
+ gtb.binds,
+ gtb.config
+ );
+
useEffect(touchBind, [touchBind]);
return (
diff --git a/src/components/MandelbrotRenderer.jsx b/src/components/MandelbrotRenderer.jsx
index b598c88..84b3924 100644
--- a/src/components/MandelbrotRenderer.jsx
+++ b/src/components/MandelbrotRenderer.jsx
@@ -1,15 +1,12 @@
import React, { useRef, useEffect } from "react";
import _ from 'lodash';
-import { addV, useGesture } from "react-use-gesture";
-import { scale } from 'vec-la';
-
+import { useGesture } from "react-use-gesture";
import { animated } from "react-spring";
-
import newSmoothMandelbrotShader from "../shaders/newSmoothMandelbrotShader";
import WebGLCanvas from "./WebGLCanvas";
-// import mShader from "../shaders/smooth_mandelbrot_shader.glsl";
+import { genericTouchBind } from "./utils";
export default function MandelbrotRenderer(props) {
@@ -32,9 +29,9 @@ export default function MandelbrotRenderer(props) {
// read incoming props
- const [{ pos }, setControlPos] = props.controls.pos;
- const [{ theta, last_pointer_angle }, setControlRot] = props.controls.rot;
- const [{ zoom, last_pointer_dist, minZoom, maxZoom }, setControlZoom] = props.controls.zoom;
+ const [{ pos }] = props.controls.pos;
+ // const [{ theta, last_pointer_angle }, setControlRot] = props.controls.rot;
+ const [{ zoom }, setControlZoom] = props.controls.zoom;
const maxI = props.maxiter;
const AA = 1;
@@ -50,102 +47,20 @@ export default function MandelbrotRenderer(props) {
radius: 30,
});
- // the hook responsible for handling gestures
- const touchBind = useGesture({
-
- // prevent some browser events such as swipe-based navigation or
- // pinch-based zoom and instead redirect them to this handler
- onDragStart: ({ event }) => event.preventDefault(),
- onPinchStart: ({ event }) => event.preventDefault(),
-
- onPinch: ({ offset: [d], down, vdva, memo = [theta.getValue(), last_pointer_angle.getValue(), zoom.getValue(), last_pointer_dist.getValue()] }) => {
- // alert(mx, my)
- // let [theta, lpa] = memo
- let [,, z, lpd] = memo;
- // console.log(d);
- let d_rel = d / 250;
- let curr_zoom = zoom.getValue();
-
- setControlZoom({
- zoom: _.clamp(z + (d_rel - lpd) * Math.sign(curr_zoom) * Math.abs(curr_zoom ** 0.9), minZoom.getValue(), maxZoom.getValue()),
- last_pointer_dist: d_rel,
-
- immediate: down,
- // config: { velocity: vd, decay: true }
- });
-
- // setControlRot({
- // theta: t + (a - lpa),
- // last_pointer_angle: a,
-
- // immediate: down,
- // // config: { velocity: va, decay: true }
- // });
-
- return memo;
- },
-
- onPinchEnd: ({ vdva: [, va] }) => {
- setControlRot({
- // set theta so it's remembered next time
- theta: va,
-
- // config: { velocity: va, decay: true }
- });
- },
-
- onWheel: ({ movement: [, my], vxvy: [, vy] }) => {
- // x, y obtained from event
- let z = zoom.getValue();
- let newZ = z * (1 - my * (my < 0 ? 2e-3 : 9e-4));
-
- setControlZoom({
- zoom: _.clamp(newZ, minZoom.getValue(), maxZoom.getValue()),
- config: {
- // bias velocity towards zooming in (vy negative )
- // if zooming
- velocity: _.clamp(vy * (vy < 0 ? 2.5 : 1.5), -10, 10), // * z**0.9 - my/15,
- }
- });
- },
-
- onDrag: ({ down, movement, velocity, direction, memo = pos.getValue() }) => {
-
- // change according to this formula:
- // move (x, y) in the opposite direction of drag (pan with cursor)
- // divide by canvas size to scale appropriately
- // multiply by 2 to correct scaling on viewport
- // use screen multiplier for more granularity
- let realZoom = gl.current.canvas.height * zoom.getValue() * screenScaleMultiplier;
-
- let plotMovement = scale(movement, -2/realZoom);
-
- let relMove = [plotMovement[0], -plotMovement[1]];
- let relDir = [direction[0], -direction[1]];
-
- setControlPos({
- pos: addV(memo, relMove), // add the displacement to the starting position
- immediate: down, // immediately apply if the gesture is active
- config: {
- velocity: scale(relDir, -velocity/realZoom), // set the velocity (gesture momentum)
- decay: true,
- },
- });
-
- return memo;
- },
-
- }, {
- event: { passive: false, capture: false },
+
+ let gtb = genericTouchBind({
domTarget: canvasRef,
- // The config object passed to useGesture has drag, wheel, scroll, pinch and move keys
- // for specific gesture options. See here for more details.
- // drag: {
- // bounds,
- // rubberband: true,
- // }
+ posControl: props.controls.pos,
+ zoomControl: props.controls.zoom,
+ screenScaleMultiplier: screenScaleMultiplier,
+ gl: gl,
});
+ let touchBind = useGesture(
+ gtb.binds,
+ gtb.config
+ );
+
useEffect(touchBind, [touchBind]);
diff --git a/src/components/utils.js b/src/components/utils.js
index 69a769c..d93a07f 100644
--- a/src/components/utils.js
+++ b/src/components/utils.js
@@ -1,4 +1,7 @@
import { useEffect, useState, useCallback } from "react";
+import { useGesture, subV, addV } from "react-use-gesture";
+import { scale } from "vec-la";
+import _ from "lodash";
// https://usehooks.com/useWindowSize/
export function useWindowSize() {
@@ -27,3 +30,102 @@ export function useWindowSize() {
return windowSize;
}
+
+// a touchbind for re-using across renderers
+export function genericTouchBind({ domTarget, posControl, zoomControl, screenScaleMultiplier, gl }) {
+ let [{ pos }, setControlPos] = posControl;
+ let [{ zoom, minZoom, maxZoom }, setControlZoom] = zoomControl;
+ return {
+ binds: {
+
+ // prevent some browser events such as swipe-based navigation or
+ // pinch-based zoom and instead redirect them to this handler
+ onDragStart: ({ event }) => event.preventDefault(),
+ onPinchStart: ({ event }) => event.preventDefault(),
+
+ onPinch: ({ vdva: [vd,], down, delta: [dx,], origin, first, memo = [pos.getValue()], z = zoom.getValue() }) => {
+ if (first) {
+ let [p] = memo;
+ return [p, origin];
+ }
+ // initial origin access
+ let [p, initialOrigin] = memo;
+ let newZ = z * (1 + dx * 5e-3);
+ let newZclamp = _.clamp(newZ, minZoom.getValue(), maxZoom.getValue());
+
+ let realZoom = gl.current.canvas.height * newZclamp * screenScaleMultiplier;
+ let plotMovement = scale(subV(origin, initialOrigin), -2/realZoom);
+ let relMove = [plotMovement[0], -plotMovement[1]];
+
+ setControlZoom({
+ zoom: newZclamp,
+ immediate: down,
+ config: {
+ // value needs revising, currently too slow
+ velocity: 10 * vd,
+ }
+ });
+
+ setControlPos({
+ pos: addV(p, relMove), // add the displacement to the starting position
+ immediate: down, // immediately apply if the gesture is active
+ });
+
+ return memo;
+ },
+
+ onWheel: ({ movement: [, my], vxvy: [, vy], active, z = zoom.getValue() }) => {
+ // x, y obtained from event
+ let newZ = z * (1 - my * (my < 0 ? 3e-4 : 2e-4));
+
+ setControlZoom({
+ zoom: _.clamp(newZ, minZoom.getValue(), maxZoom.getValue()),
+ immediate: active,
+ config: {
+ // velocity: active ? 0 : 10,
+ }
+ });
+
+ return z;
+ },
+
+ onDrag: ({ down, movement, velocity, direction, pinching, memo = pos.getValue() }) => {
+
+ // let pinch handle movement
+ if (pinching) return;
+ // change according to this formula:
+ // move (x, y) in the opposite direction of drag (pan with cursor)
+ // divide by canvas size to scale appropriately
+ // multiply by 2 to correct scaling on viewport
+ // use screen multiplier for more granularity
+ let realZoom = gl.current.canvas.height * zoom.getValue() * screenScaleMultiplier;
+
+ let plotMovement = scale(movement, -2/realZoom);
+
+ let relMove = [plotMovement[0], -plotMovement[1]];
+ let relDir = [direction[0], -direction[1]];
+
+ setControlPos({
+ pos: addV(memo, relMove), // add the displacement to the starting position
+ immediate: down, // immediately apply if the gesture is active
+ config: {
+ velocity: scale(relDir, -velocity/realZoom), // set the velocity (gesture momentum)
+ decay: true,
+ },
+ });
+
+ return memo;
+ },
+
+ },
+ config: {
+ event: { passive: false, capture: false },
+ domTarget: domTarget,
+ // The config object passed to useGesture has drag, wheel, scroll, pinch and move keys
+ // for specific gesture options. See here for more details.
+ // drag: {
+ // bounds,
+ // rubberband: true,
+ // }
+ }};
+}
\ No newline at end of file
From f49f37c239c19dc2efe480817af408ee0c5048ad Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 12:44:04 +0000
Subject: [PATCH 02/38] add material ui icons, lab
---
package.json | 4 +++-
yarn.lock | 20 +++++++++++++++++++-
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index eb64199..0fab7db 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,8 @@
"homepage": "https://jmaio.github.io/mandelbrot-maps/",
"dependencies": {
"@material-ui/core": "^4.4.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "^4.0.0-alpha.44",
"@mdi/js": "^4.4.95",
"complex.js": "^2.0.11",
"gl-matrix": "^3.1.0",
@@ -49,4 +51,4 @@
"jest-canvas-mock": "^2.2.0",
"jest-webgl-canvas-mock": "^0.2.3"
}
-}
\ No newline at end of file
+}
diff --git a/yarn.lock b/yarn.lock
index b5fe03d..e6da1c3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1130,6 +1130,24 @@
react-is "^16.8.0"
react-transition-group "^4.3.0"
+"@material-ui/icons@^4.9.1":
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.9.1.tgz#fdeadf8cb3d89208945b33dbc50c7c616d0bd665"
+ integrity sha512-GBitL3oBWO0hzBhvA9KxqcowRUsA0qzwKkURyC8nppnC3fw54KPKZ+d4V1Eeg/UnDRSzDaI9nGCdel/eh9AQMg==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+
+"@material-ui/lab@^4.0.0-alpha.44":
+ version "4.0.0-alpha.44"
+ resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.44.tgz#041281a288f731594a46c1010477e82cad4925fd"
+ integrity sha512-reKjuN9E6bDe0FLqAdaki68sKa9Lbrz+P6S2ZBIs1CP9rVd48rL1VHOonllcSEuTd4vM+apJxW8LxINJ5WsaFg==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ "@material-ui/utils" "^4.7.1"
+ clsx "^1.0.4"
+ prop-types "^15.7.2"
+ react-is "^16.8.0"
+
"@material-ui/styles@^4.9.0":
version "4.9.0"
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.0.tgz#10c31859f6868cfa9d3adf6b6c3e32c9d676bc76"
@@ -2581,7 +2599,7 @@ clone-deep@^2.0.1:
kind-of "^6.0.0"
shallow-clone "^1.0.0"
-clsx@^1.0.2:
+clsx@^1.0.2, clsx@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
From cb36a89cb8abbb51a07411c78fbbde7ff616e11f Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 12:49:05 +0000
Subject: [PATCH 03/38] create prototype menus
---
src/App.js | 15 ++-
src/components/SettingsMenu.jsx | 148 +++++++++++++++++++++++++++
src/components/SettingsSpeedDial.jsx | 94 +++++++++++++++++
3 files changed, 255 insertions(+), 2 deletions(-)
create mode 100644 src/components/SettingsMenu.jsx
create mode 100644 src/components/SettingsSpeedDial.jsx
diff --git a/src/App.js b/src/App.js
index 7a7a0c9..166facb 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import './App.css';
-import { Grid, Card, Typography } from '@material-ui/core';
+import { Grid, Card, Typography, Fab } from '@material-ui/core';
import ZoomBar from './components/ZoomBar';
import IterationSlider from './components/IterationSlider';
import RotationControl from './components/RotationControl';
@@ -10,7 +10,8 @@ import MandelbrotRenderer from './components/MandelbrotRenderer.jsx';
import { useSpring, animated } from 'react-spring';
import JuliaRenderer from './components/JuliaRenderer';
import { useWindowSize } from './components/utils';
-
+import SettingsSpeedDial from "./components/SettingsSpeedDial";
+import SettingsMenu from "./components/SettingsMenu";
function App() {
@@ -160,6 +161,16 @@ function App() {
}}
/>
+
+ {/*
+
+ */}
);
}
diff --git a/src/components/SettingsMenu.jsx b/src/components/SettingsMenu.jsx
new file mode 100644
index 0000000..18d79fe
--- /dev/null
+++ b/src/components/SettingsMenu.jsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import Button from '@material-ui/core/Button';
+import Menu from '@material-ui/core/Menu';
+import MenuItem from '@material-ui/core/MenuItem';
+import { makeStyles, Fab, Switch, Popover, FormGroup, FormControlLabel, Slider, Typography, Grid, Divider, Backdrop } from '@material-ui/core';
+import SettingsIcon from '@material-ui/icons/Settings';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ position: 'absolute',
+ bottom: theme.spacing(2),
+ right: theme.spacing(2),
+ // position: 'absolute',
+ // bottom: 0,
+ // right: 0,
+ zIndex: 2,
+ },
+ speedDial: {
+ // position: 'absolute',
+ // bottom: theme.spacing(1),
+ // right: theme.spacing(1),
+ },
+ sliderControl: {
+ width: 30,
+ }
+}));
+
+let marks = [
+ { value: 200, label: 200 },
+ { value: 800, label: 800 },
+]
+
+export default function SettingsMenu(props) {
+ const classes = useStyles();
+
+ const [anchorEl, setAnchorEl] = React.useState(null);
+
+ const handleClick = event => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ let items = [
+ // [
+ {name: 'Anti-aliasing', option: },
+ // {name: 'Iterations', option: },
+ {name: 'Split view', option: },
+ {name: 'Show coordinates', option: },
+ // ]
+ ]
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/SettingsSpeedDial.jsx b/src/components/SettingsSpeedDial.jsx
new file mode 100644
index 0000000..ad31493
--- /dev/null
+++ b/src/components/SettingsSpeedDial.jsx
@@ -0,0 +1,94 @@
+import React, { useState } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import SpeedDial from '@material-ui/lab/SpeedDial';
+import SettingsIcon from '@material-ui/icons/Settings';
+import SpeedDialAction from '@material-ui/lab/SpeedDialAction';
+import BlurLinearIcon from '@material-ui/icons/BlurLinear';
+import TimelineIcon from '@material-ui/icons/Timeline';
+import InfoIcon from '@material-ui/icons/Info';
+import SwapHorizIcon from '@material-ui/icons/SwapHoriz';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ position: 'absolute',
+ bottom: theme.spacing(2),
+ right: theme.spacing(1),
+ // position: 'absolute',
+ // bottom: 0,
+ // right: 0,
+ zIndex: 2,
+ },
+ speedDial: {
+ // position: 'absolute',
+ // bottom: theme.spacing(1),
+ // right: theme.spacing(1),
+ },
+}));
+
+
+export default function SettingsSpeedDial() {
+
+ const actions = [
+ { icon: , name: 'Antialiasing', enabled: useState(true) },
+ { icon: , name: 'Iterations', enabled: useState(true) },
+ { icon: , name: 'View', enabled: useState(true) },
+ { icon: , name: 'About', enabled: useState(true) },
+ // { icon: , name: 'Save' },
+ // { icon: , name: 'Print' },
+ // { icon: , name: 'Share' },
+ // { icon: , name: 'Like' },
+ ];
+
+ const classes = useStyles();
+ const [open, setOpen] = React.useState(false);
+
+ const handleOpen = (e, reason) => {
+ if (reason === "toggle") {
+ setOpen(true);
+ };
+ };
+
+ const handleClose = (e, reason) => {
+ if (["toggle", "escapeKeyDown"].includes(reason)) {
+ setOpen(false);
+ };
+ };
+
+ return (
+
+ }
+ onClose={handleClose}
+ onOpen={handleOpen}
+ open={open}
+ // TransitionComponent={}
+ FabProps={{
+ size: "small",
+ style: {
+ backgroundColor: "#2773bb",
+ }
+ }}
+ >
+ {actions.map(action => (
+ {
+ console.log(action.enabled[0]);
+ action.enabled[1](!action.enabled[0]);
+ }}
+ FabProps={{
+ style: {
+ backgroundColor: action.enabled[0] ? "#4fc3f7" : "#f8f8f8",
+ }
+ }}
+ />
+ ))}
+
+
+ );
+}
From 17910fb3bacb7a7199deae6922d59711a3d651b9 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 14:05:37 +0000
Subject: [PATCH 04/38] remove pinch move from touchbind (zooms into centre,
feels okay)
---
src/components/utils.js | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/components/utils.js b/src/components/utils.js
index d93a07f..df03430 100644
--- a/src/components/utils.js
+++ b/src/components/utils.js
@@ -49,13 +49,13 @@ export function genericTouchBind({ domTarget, posControl, zoomControl, screenSca
return [p, origin];
}
// initial origin access
- let [p, initialOrigin] = memo;
+ // let [p, initialOrigin] = memo;
let newZ = z * (1 + dx * 5e-3);
let newZclamp = _.clamp(newZ, minZoom.getValue(), maxZoom.getValue());
- let realZoom = gl.current.canvas.height * newZclamp * screenScaleMultiplier;
- let plotMovement = scale(subV(origin, initialOrigin), -2/realZoom);
- let relMove = [plotMovement[0], -plotMovement[1]];
+ // let realZoom = gl.current.canvas.height * newZclamp * screenScaleMultiplier;
+ // let plotMovement = scale(subV(origin, initialOrigin), -2/realZoom);
+ // let relMove = [plotMovement[0], -plotMovement[1]];
setControlZoom({
zoom: newZclamp,
@@ -66,10 +66,10 @@ export function genericTouchBind({ domTarget, posControl, zoomControl, screenSca
}
});
- setControlPos({
- pos: addV(p, relMove), // add the displacement to the starting position
- immediate: down, // immediately apply if the gesture is active
- });
+ // setControlPos({
+ // pos: addV(p, relMove), // add the displacement to the starting position
+ // immediate: down, // immediately apply if the gesture is active
+ // });
return memo;
},
From 81ff96d41c82a6377cd1190af80ea886db7d8aa3 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 14:12:19 +0000
Subject: [PATCH 05/38] make mini viewers more visible on low zoom
---
src/components/JuliaRenderer.jsx | 4 ++--
src/components/MandelbrotRenderer.jsx | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/components/JuliaRenderer.jsx b/src/components/JuliaRenderer.jsx
index 8543522..17ca4ca 100644
--- a/src/components/JuliaRenderer.jsx
+++ b/src/components/JuliaRenderer.jsx
@@ -80,8 +80,8 @@ export default function JuliaRenderer(props) {
// border: "1px solid #000",
boxShadow: "0px 2px 10px 1px rgba(0, 0, 0, 0.4)",
overflow: "hidden",
- opacity: zoom.interpolate(z => _.clamp(z / 5 - 0.5, 0, 1)),
- display: zoom.interpolate(z => _.clamp(z / 5 - 0.5, 0, 1) === 0 ? "none" : "block"),
+ opacity: zoom.interpolate(z => _.clamp(z - 0.5, 0, 1)),
+ display: zoom.interpolate(z => _.clamp(z - 0.5, 0, 1) === 0 ? "none" : "block"),
}}
onClick={() => setControlZoom({ zoom: 1 })}
>
diff --git a/src/components/MandelbrotRenderer.jsx b/src/components/MandelbrotRenderer.jsx
index 84b3924..57029d7 100644
--- a/src/components/MandelbrotRenderer.jsx
+++ b/src/components/MandelbrotRenderer.jsx
@@ -93,8 +93,8 @@ export default function MandelbrotRenderer(props) {
// border: "1px solid #000",
boxShadow: "0px 2px 10px 1px rgba(0, 0, 0, 0.4)",
overflow: "hidden",
- opacity: zoom.interpolate(z => _.clamp(z / 10 - 0.5, 0, 1)),
- display: zoom.interpolate(z => _.clamp(z / 10 - 0.5, 0, 1) === 0 ? "none" : "block"),
+ opacity: zoom.interpolate(z => _.clamp(z - 1.5, 0, 1)),
+ display: zoom.interpolate(z => _.clamp(z - 1.5, 0, 1) === 0 ? "none" : "block"),
}}
onClick={() => setControlZoom({ zoom: 1 })}
>
From fa4e958175977197e60c70fa9326ed3655ebb275 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 14:29:43 +0000
Subject: [PATCH 06/38] change column orientation for renderers (mandelbrot
second)
---
src/App.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/App.js b/src/App.js
index 166facb..af56b14 100644
--- a/src/App.js
+++ b/src/App.js
@@ -89,7 +89,7 @@ function App() {
Date: Mon, 24 Feb 2020 14:34:18 +0000
Subject: [PATCH 07/38] move settings to App
---
src/App.js | 39 +++++++++++++++++-
src/components/SettingsMenu.jsx | 72 +++++++++++++++------------------
2 files changed, 69 insertions(+), 42 deletions(-)
diff --git a/src/App.js b/src/App.js
index af56b14..dce3f2a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import './App.css';
-import { Grid, Card, Typography, Fab } from '@material-ui/core';
+import { Grid, Card, Typography, Fab, Slider, Switch } from '@material-ui/core';
import ZoomBar from './components/ZoomBar';
import IterationSlider from './components/IterationSlider';
import RotationControl from './components/RotationControl';
@@ -82,6 +82,41 @@ function App() {
})),
};
+ let settings = [{
+ title: "Interface",
+ items: {
+ coords: {
+ name: 'Show coordinates',
+ ctrl:
+ },
+ miniViewer: {
+ name: 'Mini viewers',
+ ctrl:
+ },
+ }
+ }, {
+ title: "Graphics",
+ items: {
+ iterations: {
+ name: 'Iterations',
+ ctrl: ,
+ placement: "top"},
+ aa: {
+ name: 'Anti-aliasing',
+ ctrl:
+ },
+ }
+ }]
// const [{ pos }, setPos] = mandelbrotControls.pos;
return (
@@ -161,7 +196,7 @@ function App() {
}}
/>
-
+
{/* ({
}
}));
-let marks = [
- { value: 200, label: 200 },
- { value: 800, label: 800 },
-]
+// let marks =
export default function SettingsMenu(props) {
const classes = useStyles();
@@ -43,14 +40,10 @@ export default function SettingsMenu(props) {
setAnchorEl(null);
};
- let items = [
- // [
- {name: 'Anti-aliasing', option: },
- // {name: 'Iterations', option: },
- {name: 'Split view', option: },
- {name: 'Show coordinates', option: },
- // ]
- ]
+
+ // {name: 'Iterations', option: },
+ // [
+ // ]
return (
@@ -74,10 +67,7 @@ export default function SettingsMenu(props) {
}}
transformOrigin={{
vertical: "bottom",
- horizontal: "center"
- }}
- style={{
- // marginBottom: 20,
+ horizontal: "right"
}}
>
- Configuration
+ Configuration
-
-
-
- {items.map((ctrl, i) =>
-
+ {props.settings.map((group, i) =>
+
+
+ {group.title}
+
+ {Object.values(group.items).map((ctrl, j) =>
+
+ )}
+
+
)}
-
-
@@ -120,7 +112,7 @@ export default function SettingsMenu(props) {
Iterations
-
+ */}
{/*
From 6b601d93600cff04a9f35dd3507e07198bd6ac62 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 18:08:32 +0000
Subject: [PATCH 08/38] hook up settings, use device pixel ratio
---
src/App.js | 108 ++++++++++++++++++++------
src/components/JuliaRenderer.jsx | 86 ++++++++++----------
src/components/MandelbrotRenderer.jsx | 20 +++--
src/components/SettingsMenu.jsx | 53 ++++++-------
src/components/WebGLCanvas.jsx | 6 +-
5 files changed, 172 insertions(+), 101 deletions(-)
diff --git a/src/App.js b/src/App.js
index dce3f2a..074a2b4 100644
--- a/src/App.js
+++ b/src/App.js
@@ -5,12 +5,11 @@ import ZoomBar from './components/ZoomBar';
import IterationSlider from './components/IterationSlider';
import RotationControl from './components/RotationControl';
-import 'typeface-roboto';
+// import 'typeface-roboto';
import MandelbrotRenderer from './components/MandelbrotRenderer.jsx';
import { useSpring, animated } from 'react-spring';
import JuliaRenderer from './components/JuliaRenderer';
import { useWindowSize } from './components/utils';
-import SettingsSpeedDial from "./components/SettingsSpeedDial";
import SettingsMenu from "./components/SettingsMenu";
function App() {
@@ -19,9 +18,8 @@ function App() {
let defaultSpringConfig = { mass: 1, tension: 100, friction: 200 };
- let maxIter = useState(250);
// eslint-disable-next-line
- let [maxI, setMaxI] = maxIter;
+ // let [maxI, setMaxI] = maxIter;
// this multiplier subdivides the screen space into smaller increments
// to allow for velocity calculations to not immediately decay, due to the
@@ -82,16 +80,53 @@ function App() {
})),
};
+ let resetPosSpringConfig = { tension: 200, friction: 75 };
+ let resetZoomSpringConfig = { tension: 300, friction: 60 };
+
+ let reset = () => {
+ mandelbrotControls.pos[1]({
+ pos: [0, 0],
+ config: resetPosSpringConfig,
+ });
+ mandelbrotControls.zoom[1]({
+ zoom: 1,
+ config: resetZoomSpringConfig,
+ });
+
+ juliaControls.pos[1]({
+ pos: [0, 0],
+ config: resetPosSpringConfig,
+ });
+ juliaControls.zoom[1]({
+ zoom: 1,
+ config: resetZoomSpringConfig,
+ });
+ }
+
+ let controls = {
+ coords: useState(false),
+ miniViewer: useState(true),
+ maxI: useState(250),
+ aa: useState(false),
+ dpr: useState(true),
+ };
+
let settings = [{
title: "Interface",
items: {
coords: {
name: 'Show coordinates',
- ctrl:
+ ctrl:
controls.coords[1](!controls.coords[0])}
+ />
},
miniViewer: {
name: 'Mini viewers',
- ctrl:
+ ctrl: controls.miniViewer[1](!controls.miniViewer[0])}
+ />
},
}
}, {
@@ -100,20 +135,36 @@ function App() {
iterations: {
name: 'Iterations',
ctrl: controls.maxI[1](val)}
+ // onChange={(e, val) => console.log(val)}
/>,
placement: "top"},
+ dpr: {
+ name: `Use device pixel ratio (${window.devicePixelRatio || 1})`,
+ ctrl: controls.dpr[1](!controls.dpr[0])}
+ />
+ },
aa: {
- name: 'Anti-aliasing',
- ctrl:
+ name: 'Anti-aliasing (slow!)',
+ ctrl: controls.aa[1](!controls.aa[0])}
+ />
},
}
}]
@@ -139,7 +190,9 @@ function App() {
right: 0,
top: 0,
margin: 20,
- padding: 5,
+ padding: 8,
+ display: controls.coords[0] ? "block" : "none",
+ // borderRadius: 100,
}}
>
@@ -150,9 +203,12 @@ function App() {
-
-
+ /> */}
-
-
-
+ */}
+ reset()}
+ />
{/*
- _.clamp(z - 0.5, 0, 1)),
- display: zoom.interpolate(z => _.clamp(z - 0.5, 0, 1) === 0 ? "none" : "block"),
- }}
- onClick={() => setControlZoom({ zoom: 1 })}
- >
-
-
+ {/* mini viewer */}
+ {props.enableMini ?
+ _.clamp(z - 1, 0, 1)),
+ display: zoom.interpolate(z => _.clamp(z - 1, 0, 1) === 0 ? "none" : "block"),
+ }}
+ onClick={() => setControlZoom({ zoom: 1 })}
+ >
+
+
+ :
+
+ }
)
}
\ No newline at end of file
diff --git a/src/components/MandelbrotRenderer.jsx b/src/components/MandelbrotRenderer.jsx
index 57029d7..328191b 100644
--- a/src/components/MandelbrotRenderer.jsx
+++ b/src/components/MandelbrotRenderer.jsx
@@ -33,11 +33,15 @@ export default function MandelbrotRenderer(props) {
// const [{ theta, last_pointer_angle }, setControlRot] = props.controls.rot;
const [{ zoom }, setControlZoom] = props.controls.zoom;
const maxI = props.maxiter;
- const AA = 1;
+ const AA = props.aa ? 2 : 1;
+
+ useEffect(() => {
+ console.log(props.dpr);
+ }, [props.dpr]);
const fragShader = newSmoothMandelbrotShader({
maxI: maxI,
- AA: AA
+ AA: AA,
});
const miniFragShader = newSmoothMandelbrotShader({
maxI: maxI,
@@ -52,7 +56,7 @@ export default function MandelbrotRenderer(props) {
domTarget: canvasRef,
posControl: props.controls.pos,
zoomControl: props.controls.zoom,
- screenScaleMultiplier: screenScaleMultiplier,
+ screenScaleMultiplier: screenScaleMultiplier / props.dpr,
gl: gl,
});
@@ -71,6 +75,7 @@ export default function MandelbrotRenderer(props) {
+ {props.enableMini ?
_.clamp(z - 1.5, 0, 1)),
- display: zoom.interpolate(z => _.clamp(z - 1.5, 0, 1) === 0 ? "none" : "block"),
+ opacity: zoom.interpolate(z => _.clamp(z - 1, 0, 1)),
+ display: zoom.interpolate(z => _.clamp(z - 1, 0, 1) === 0 ? "none" : "block"),
}}
onClick={() => setControlZoom({ zoom: 1 })}
>
+ :
+ }
)
diff --git a/src/components/SettingsMenu.jsx b/src/components/SettingsMenu.jsx
index e079382..068ec54 100644
--- a/src/components/SettingsMenu.jsx
+++ b/src/components/SettingsMenu.jsx
@@ -2,19 +2,25 @@ import React from 'react';
import Button from '@material-ui/core/Button';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
-import { makeStyles, Fab, Switch, Popover, FormGroup, FormControlLabel, Slider, Typography, Grid, Divider, Backdrop } from '@material-ui/core';
+import { makeStyles, Fab, Switch, Popover, FormGroup, FormControlLabel, Slider, Typography, Grid, Divider, Backdrop, Box } from '@material-ui/core';
import SettingsIcon from '@material-ui/icons/Settings';
+import MyLocationIcon from '@material-ui/icons/MyLocation';
const useStyles = makeStyles(theme => ({
root: {
position: 'absolute',
bottom: theme.spacing(2),
right: theme.spacing(2),
+ display: "flex",
+ flexDirection: "column",
// position: 'absolute',
// bottom: 0,
// right: 0,
zIndex: 2,
},
+ button: {
+ marginTop: 10,
+ },
speedDial: {
// position: 'absolute',
// bottom: theme.spacing(1),
@@ -40,17 +46,13 @@ export default function SettingsMenu(props) {
setAnchorEl(null);
};
-
- // {name: 'Iterations', option: },
- // [
- // ]
-
return (
@@ -60,6 +62,7 @@ export default function SettingsMenu(props) {
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
+ // open={true}
onClose={handleClose}
anchorOrigin={{
horizontal: "right",
@@ -76,15 +79,15 @@ export default function SettingsMenu(props) {
paddingTop: "1em",
paddingBottom: "1em",
}}>
-
Configuration
-
+
Configuration
+
{props.settings.map((group, i) =>
- {group.title}
+ {group.title}
{Object.values(group.items).map((ctrl, j) =>
)}
- {/*
-
-
- Iterations
-
- (i ** 1.5).toFixed(0)}
- valueLabelDisplay="auto"
- // style={{
- // // paddingLeft: 4,
- // // paddingRight: 4,
- // }}
-
- // track={false}
-
- marks={marks}
- />
- */}
+
{/*
diff --git a/src/components/WebGLCanvas.jsx b/src/components/WebGLCanvas.jsx
index a845e90..168c07c 100644
--- a/src/components/WebGLCanvas.jsx
+++ b/src/components/WebGLCanvas.jsx
@@ -25,6 +25,8 @@ export default React.forwardRef(({mini = false, ...props}, ref) => {
const zoom = mini ? () => 1.0 : () => props.u.zoom.getValue();
const currZoom = useRef(zoom);
+ const dpr = props.dpr || 1;
+
useEffect(() => {
currZoom.current = props.u.zoom.getValue();
}, [props.u]);
@@ -38,7 +40,7 @@ export default React.forwardRef(({mini = false, ...props}, ref) => {
// the main render function for WebGL
const render = useCallback(time => {
- twgl.resizeCanvasToDisplaySize(gl.current.canvas);
+ twgl.resizeCanvasToDisplaySize(gl.current.canvas, dpr);
gl.current.viewport(0, 0, gl.current.canvas.width, gl.current.canvas.height);
const uniforms = {
@@ -55,7 +57,7 @@ export default React.forwardRef(({mini = false, ...props}, ref) => {
twgl.drawBufferInfo(gl.current, bufferInfo.current);
// The 'state' will always be the initial value here
renderRequestRef.current = requestAnimationFrame(render);
- }, [gl, props.u, zoom]);
+ }, [gl, props.u, zoom, dpr]);
// re-compile program if shader changes
useEffect(() => {
From ec6e431c6d96a210f21c637e27c481f2980b1300 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 21:42:20 +0000
Subject: [PATCH 09/38] tweak settings
---
src/App.js | 53 +++++++--------------------------
src/components/SettingsMenu.jsx | 25 ++++++++++++++--
2 files changed, 33 insertions(+), 45 deletions(-)
diff --git a/src/App.js b/src/App.js
index 074a2b4..df0d5de 100644
--- a/src/App.js
+++ b/src/App.js
@@ -25,6 +25,7 @@ function App() {
// to allow for velocity calculations to not immediately decay, due to the
// otherwise small scale that is being mapped to the screen.
const screenScaleMultiplier = 1e-7;
+ const [dpr, setDpr] = useState(window.devicePixelRatio || 1);
const startPos = [-0.743030, 0.126433];
// const startPos = [-.7426482, .1271875 ];
@@ -153,14 +154,19 @@ function App() {
/>,
placement: "top"},
dpr: {
- name: `Use device pixel ratio (${window.devicePixelRatio || 1})`,
+ name: `Use pixel ratio (${window.devicePixelRatio || 1})`,
ctrl:
controls.dpr[1](!controls.dpr[0])}
+ onChange={() => {
+ let useDpr = !controls.dpr[0];
+ // console.log(useDpr ? window.devicePixelRatio : 1);
+ setDpr(useDpr ? window.devicePixelRatio : 1)
+ controls.dpr[1](useDpr);
+ }}
/>
},
aa: {
- name: 'Anti-aliasing (slow!)',
+ name: 'Anti-aliasing (slow)',
ctrl: controls.aa[1](!controls.aa[0])}
@@ -208,7 +214,7 @@ function App() {
miniSize={miniSize}
enableMini={controls.miniViewer[0]}
aa={controls.aa[0]}
- dpr={controls.dpr[0] ? window.devicePixelRatio : 1}
+ dpr={dpr}
/>
- {/*
- {/*
-
- */}
- {/*
- */}
reset()}
/>
- {/*
-
- */}
);
}
diff --git a/src/components/SettingsMenu.jsx b/src/components/SettingsMenu.jsx
index 068ec54..c0639af 100644
--- a/src/components/SettingsMenu.jsx
+++ b/src/components/SettingsMenu.jsx
@@ -2,9 +2,10 @@ import React from 'react';
import Button from '@material-ui/core/Button';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
-import { makeStyles, Fab, Switch, Popover, FormGroup, FormControlLabel, Slider, Typography, Grid, Divider, Backdrop, Box } from '@material-ui/core';
+import { makeStyles, Fab, Switch, Popover, FormGroup, FormControlLabel, Slider, Typography, Grid, Divider, Backdrop, Box, IconButton } from '@material-ui/core';
import SettingsIcon from '@material-ui/icons/Settings';
import MyLocationIcon from '@material-ui/icons/MyLocation';
+import InfoIcon from '@material-ui/icons/Info';
const useStyles = makeStyles(theme => ({
root: {
@@ -79,7 +80,24 @@ export default function SettingsMenu(props) {
paddingTop: "1em",
paddingBottom: "1em",
}}>
- Configuration
+
+
+
+ Configuration
+
+
+
+ {/*
+ */}
+
+ {
+ setAnchorEl(null);
+ }}>
+
+
+
+
+
{props.settings.map((group, i) =>
@@ -87,7 +105,8 @@ export default function SettingsMenu(props) {
marginTop: 10,
marginBottom: 10,
}} />
- {group.title}
+ {group.title}
+ {/* */}
{Object.values(group.items).map((ctrl, j) =>
Date: Mon, 24 Feb 2020 21:42:47 +0000
Subject: [PATCH 10/38] add popup info dialog
---
src/App.js | 10 ++++
src/components/InfoDialog.jsx | 88 +++++++++++++++++++++++++++++++++
src/components/SettingsMenu.jsx | 1 +
3 files changed, 99 insertions(+)
create mode 100644 src/components/InfoDialog.jsx
diff --git a/src/App.js b/src/App.js
index df0d5de..7ca62cc 100644
--- a/src/App.js
+++ b/src/App.js
@@ -12,6 +12,8 @@ import JuliaRenderer from './components/JuliaRenderer';
import { useWindowSize } from './components/utils';
import SettingsMenu from "./components/SettingsMenu";
+import InfoDialog from './components/InfoDialog';
+
function App() {
const size = useWindowSize();
@@ -104,6 +106,10 @@ function App() {
});
}
+ const [showInfo, setShowInfo] = useState(false)
+
+ let toggleInfo = () => setShowInfo(!showInfo);
+
let controls = {
coords: useState(false),
miniViewer: useState(true),
@@ -236,7 +242,11 @@ function App() {
reset()}
+ toggleInfo={() => toggleInfo()}
/>
+
+
+
);
}
diff --git a/src/components/InfoDialog.jsx b/src/components/InfoDialog.jsx
new file mode 100644
index 0000000..0bcc306
--- /dev/null
+++ b/src/components/InfoDialog.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { withStyles } from '@material-ui/core/styles';
+import Button from '@material-ui/core/Button';
+import Dialog from '@material-ui/core/Dialog';
+import MuiDialogTitle from '@material-ui/core/DialogTitle';
+import MuiDialogContent from '@material-ui/core/DialogContent';
+import MuiDialogActions from '@material-ui/core/DialogActions';
+import IconButton from '@material-ui/core/IconButton';
+import CloseIcon from '@material-ui/icons/Close';
+import Typography from '@material-ui/core/Typography';
+
+const styles = theme => ({
+ root: {
+ margin: 0,
+ padding: theme.spacing(2),
+ },
+ closeButton: {
+ position: 'absolute',
+ right: theme.spacing(1),
+ top: theme.spacing(1),
+ color: theme.palette.grey[500],
+ },
+});
+
+const DialogTitle = withStyles(styles)(props => {
+ const { children, classes, onClose, ...other } = props;
+ return (
+
+ {children}
+ {onClose ? (
+
+
+
+ ) : null}
+
+ );
+});
+
+const DialogContent = withStyles(theme => ({
+ root: {
+ padding: theme.spacing(2),
+ },
+}))(MuiDialogContent);
+
+const DialogActions = withStyles(theme => ({
+ root: {
+ margin: 0,
+ padding: theme.spacing(1),
+ },
+}))(MuiDialogActions);
+
+export default function InfoDialog(props) {
+ const [open, setOpen] = props.ctrl;
+
+ const handleClickOpen = () => {
+ setOpen(true);
+ };
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/SettingsMenu.jsx b/src/components/SettingsMenu.jsx
index c0639af..4972e8c 100644
--- a/src/components/SettingsMenu.jsx
+++ b/src/components/SettingsMenu.jsx
@@ -91,6 +91,7 @@ export default function SettingsMenu(props) {
*/}
{
+ props.toggleInfo();
setAnchorEl(null);
}}>
From 3ae30a5433afa01c4d292693c5bce7e0fe149592 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Mon, 24 Feb 2020 22:09:36 +0000
Subject: [PATCH 11/38] settings and dialog text tweaks
---
src/components/InfoDialog.jsx | 8 ++++----
src/components/SettingsMenu.jsx | 8 +++-----
2 files changed, 7 insertions(+), 9 deletions(-)
diff --git a/src/components/InfoDialog.jsx b/src/components/InfoDialog.jsx
index 0bcc306..da30b32 100644
--- a/src/components/InfoDialog.jsx
+++ b/src/components/InfoDialog.jsx
@@ -62,15 +62,15 @@ export default function InfoDialog(props) {
return (
- {/*
- */}
{
props.toggleInfo();
@@ -104,9 +102,9 @@ export default function SettingsMenu(props) {
- {group.title}
+ {group.title}
{/* */}
{Object.values(group.items).map((ctrl, j) =>
@@ -127,7 +125,7 @@ export default function SettingsMenu(props) {
+ {/* Close */}
);
From 1f08ed69332a34ecaae58f378b203f4a82b473ec Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 14:38:02 +0000
Subject: [PATCH 16/38] rename browser -> device
---
src/components/InfoDialog.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/InfoDialog.jsx b/src/components/InfoDialog.jsx
index a1a7be2..5c7dc55 100644
--- a/src/components/InfoDialog.jsx
+++ b/src/components/InfoDialog.jsx
@@ -85,7 +85,7 @@ export default function InfoDialog(props) {
- Browser properties
+ Device properties
From 5998db2955258352dd1e7f6fd75d20a586829472 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 15:44:38 +0000
Subject: [PATCH 17/38] add fallback copy to clipboard action
---
src/components/InfoDialog.jsx | 30 +++++++++++++++++++++++++-----
1 file changed, 25 insertions(+), 5 deletions(-)
diff --git a/src/components/InfoDialog.jsx b/src/components/InfoDialog.jsx
index 5c7dc55..5d097ad 100644
--- a/src/components/InfoDialog.jsx
+++ b/src/components/InfoDialog.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
@@ -8,9 +8,10 @@ import MuiDialogActions from '@material-ui/core/DialogActions';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography';
-import { Link, TableContainer, Table, Paper, TableRow, TableCell, TableHead, TableBody, Box, Divider } from '@material-ui/core';
+import { Link, TableContainer, Table, Paper, TableRow, TableCell, TableHead, TableBody, Box, Divider, Snackbar } from '@material-ui/core';
import LaunchIcon from '@material-ui/icons/Launch';
import FileCopyIcon from '@material-ui/icons/FileCopy';
+import MuiAlert from '@material-ui/lab/Alert';
const styles = theme => ({
root: {
@@ -52,13 +53,27 @@ const DialogActions = withStyles(theme => ({
},
}))(MuiDialogActions);
+function Alert(props) {
+ return ;
+}
+
export default function InfoDialog(props) {
const [open, setOpen] = props.ctrl;
+ const [snackBarOpen, setSnackBarOpen] = useState(false);
const handleClose = () => setOpen(false);
const clientData = window.jscd;
+ let writeToClipboard = data => {
+ try {
+ navigator.clipboard.writeText(data);
+ setSnackBarOpen(true);
+ } catch (e) {
+ window.prompt("Auto copy to clipboard failed, copy manually from below:", data)
+ }
+ }
+
return (
@@ -100,10 +115,15 @@ export default function InfoDialog(props) {
+
- {navigator.clipboard.writeText(JSON.stringify(clientData))}} color="primary" variant="outlined" startIcon={}>Copy
}>Feedback
- {/* Close */}
+ {writeToClipboard(JSON.stringify(clientData))}} color="primary" variant="outlined" startIcon={}>Copy
+ setSnackBarOpen(false)}>
+ setSnackBarOpen(true)} severity="info">
+ Device properties copied!
+
+
);
From 561803fc55fb78713c32cfb5d1eb5f3d7518c115 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 15:44:53 +0000
Subject: [PATCH 18/38] make survey link external json property
---
src/components/InfoDialog.jsx | 4 +++-
src/components/surveyLink.json | 3 +++
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 src/components/surveyLink.json
diff --git a/src/components/InfoDialog.jsx b/src/components/InfoDialog.jsx
index 5d097ad..8f88c3b 100644
--- a/src/components/InfoDialog.jsx
+++ b/src/components/InfoDialog.jsx
@@ -12,6 +12,8 @@ import { Link, TableContainer, Table, Paper, TableRow, TableCell, TableHead, Tab
import LaunchIcon from '@material-ui/icons/Launch';
import FileCopyIcon from '@material-ui/icons/FileCopy';
import MuiAlert from '@material-ui/lab/Alert';
+import { surveyLink } from './surveyLink.json';
+
const styles = theme => ({
root: {
@@ -117,13 +119,13 @@ export default function InfoDialog(props) {
- }>Feedback
{writeToClipboard(JSON.stringify(clientData))}} color="primary" variant="outlined" startIcon={}>Copy
setSnackBarOpen(false)}>
setSnackBarOpen(true)} severity="info">
Device properties copied!
+ }>Feedback
);
diff --git a/src/components/surveyLink.json b/src/components/surveyLink.json
new file mode 100644
index 0000000..ad2ffbf
--- /dev/null
+++ b/src/components/surveyLink.json
@@ -0,0 +1,3 @@
+{
+ "surveyLink": "#"
+}
\ No newline at end of file
From 5833bbf12e7e6464ca7246efbb071bbdf0e920b5 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 15:47:12 +0000
Subject: [PATCH 19/38] Update clientDetect.js + accreditation for gpu support
---
public/clientDetect.js | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/public/clientDetect.js b/public/clientDetect.js
index 4df119d..d8f5afc 100644
--- a/public/clientDetect.js
+++ b/public/clientDetect.js
@@ -161,10 +161,12 @@
break;
}
+ // user agent string manipulation to obtain device model
var system = nAgt.substring(nAgt.indexOf('(') + 1, nAgt.indexOf(')'));
var device = system.substring(system.lastIndexOf(';') + 1);
- // gpu profiler
+ // use JavaScript to detect GPU used from within your browser - by cvan
+ // https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8
var canvas = document.createElement('canvas');
var gl;
var debugInfo;
@@ -180,6 +182,7 @@
gpuVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
}
+ // ----------------------------------------------------------------
window.jscd = {
browser: browser,
From fbbad188c6ce32c12b9cc6ce56cef5ae745a957b Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 15:56:31 +0000
Subject: [PATCH 20/38] guard against null / undefined window in device
properties
---
src/components/InfoDialog.jsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/components/InfoDialog.jsx b/src/components/InfoDialog.jsx
index 8f88c3b..364506b 100644
--- a/src/components/InfoDialog.jsx
+++ b/src/components/InfoDialog.jsx
@@ -65,7 +65,8 @@ export default function InfoDialog(props) {
const handleClose = () => setOpen(false);
- const clientData = window.jscd;
+ // guard against null / undefined window
+ const clientData = window.jscd || {};
let writeToClipboard = data => {
try {
From 4453337ec8114cfa6cae4b6268c2a555d1acd770 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 16:17:00 +0000
Subject: [PATCH 21/38] fix properties alert onClose
---
src/components/InfoDialog.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/InfoDialog.jsx b/src/components/InfoDialog.jsx
index 364506b..2104470 100644
--- a/src/components/InfoDialog.jsx
+++ b/src/components/InfoDialog.jsx
@@ -122,7 +122,7 @@ export default function InfoDialog(props) {
{writeToClipboard(JSON.stringify(clientData))}} color="primary" variant="outlined" startIcon={}>Copy
setSnackBarOpen(false)}>
- setSnackBarOpen(true)} severity="info">
+ setSnackBarOpen(false)} severity="info">
Device properties copied!
From 36f92608faaf8627af1f9bac927b6b458f02b30c Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 17:38:06 +0000
Subject: [PATCH 22/38] add fps counter
---
src/App.js | 1 +
src/components/MandelbrotRenderer.jsx | 13 ++++++++-
src/components/WebGLCanvas.jsx | 42 ++++++++++++++++++++++++---
3 files changed, 51 insertions(+), 5 deletions(-)
diff --git a/src/App.js b/src/App.js
index e18433c..058f9f6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -220,6 +220,7 @@ function App() {
enableMini={controls.miniViewer[0]}
aa={controls.aa[0]}
dpr={dpr}
+ showFps={true}
/>
+ {props.showFps ?
+
+
+ {fps}
+
+ :
+ null
+ }
{props.enableMini ?
{
const bufferInfo = useRef(null);
const programInfo = useRef(null);
+ const u = props.u;
+ const setFps = props.fps;
+
// have a zoom callback
const zoom = mini ? () => 1.0 : () => props.u.zoom.getValue();
const currZoom = useRef(zoom);
@@ -38,6 +41,12 @@ export default React.forwardRef(({mini = false, ...props}, ref) => {
bufferInfo.current = twgl.createBufferInfoFromArrays(gl.current, fullscreenVertexArray);
}, [gl, ref]);
+
+ let then = useRef(0);
+ let frames = useRef(0);
+ let elapsedTime = useRef(0);
+ let interval = 1000;
+ // let mult = 1000 / interval;
// the main render function for WebGL
const render = useCallback(time => {
twgl.resizeCanvasToDisplaySize(gl.current.canvas, dpr);
@@ -46,18 +55,43 @@ export default React.forwardRef(({mini = false, ...props}, ref) => {
const uniforms = {
resolution: [gl.current.canvas.width, gl.current.canvas.height],
u_zoom: zoom(),
- u_c: props.u.c === undefined ? 0 : props.u.c.getValue().map(x => x * props.u.screenScaleMultiplier),
- u_pos: scale(props.u.pos.getValue(), props.u.screenScaleMultiplier),
- u_maxI: props.u.maxI,
+ u_c: u.c === undefined ? 0 : u.c.getValue().map(x => x * u.screenScaleMultiplier),
+ u_pos: scale(u.pos.getValue(), u.screenScaleMultiplier),
+ u_maxI: u.maxI,
};
gl.current.useProgram(programInfo.current.program);
twgl.setBuffersAndAttributes(gl.current, programInfo.current, bufferInfo.current);
twgl.setUniforms(programInfo.current, uniforms);
twgl.drawBufferInfo(gl.current, bufferInfo.current);
+
+ // calculate fps
+ if (setFps !== undefined) {
+ frames.current++;
+ elapsedTime.current += (time - then.current);
+ then.current = time;
+
+ // console.log(elapsedTime.current);
+ if (elapsedTime.current >= interval) {
+ //multiply with (1000 / elapsed) for accuracy
+ setFps((frames.current * (interval / elapsedTime.current)).toFixed(1));
+ frames.current = 0;
+ elapsedTime.current -= interval;
+
+ // document.getElementById('test').innerHTML = fps;
+ }
+ // time *= 0.001; // convert to seconds
+ // const deltaTime = time - then.current; // compute time since last frame
+ // then.current = time; // remember time for next frame
+ // const fs = 1 / deltaTime; // compute frames per second
+ // console.log(fs);
+ // (fs.toFixed(1)); // update fps display
+ }
+
// The 'state' will always be the initial value here
renderRequestRef.current = requestAnimationFrame(render);
- }, [gl, props.u, zoom, dpr]);
+
+ }, [gl, u, zoom, dpr, setFps]);
// re-compile program if shader changes
useEffect(() => {
From 6e2badac4085c200ac391cd76ae6e9ea22804a72 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 17:46:36 +0000
Subject: [PATCH 23/38] hook up fps meter (and clean up some settings)
---
src/App.js | 22 +++++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/src/App.js b/src/App.js
index 058f9f6..ad25fac 100644
--- a/src/App.js
+++ b/src/App.js
@@ -111,8 +111,11 @@ function App() {
maxI: useState(250),
aa: useState(false),
dpr: useState(false),
+ fps: useState(false),
};
+ let toggleVal = ([v, setV]) => setV(!v);
+
let settings = [{
title: "Interface",
items: {
@@ -121,7 +124,7 @@ function App() {
ctrl: controls.miniViewer[1](!controls.miniViewer[0])}
+ onChange={() => toggleVal(controls.miniViewer)}
/>
},
coords: {
@@ -129,7 +132,7 @@ function App() {
ctrl: controls.coords[1](!controls.coords[0])}
+ onChange={() => toggleVal(controls.coords)}
/>
},
}
@@ -155,7 +158,8 @@ function App() {
onChange={(e, val) => controls.maxI[1](val)}
// onChange={(e, val) => console.log(val)}
/>,
- placement: "top"},
+ placement: "top"
+ },
dpr: {
name: `Use pixel ratio (${window.devicePixelRatio || 1})`,
ctrl: controls.aa[1](!controls.aa[0])}
+ onChange={() => toggleVal(controls.aa)}
+ />
+ },
+ fps: {
+ name: 'Show fps',
+ ctrl: toggleVal(controls.fps)}
/>
},
}
@@ -220,7 +232,7 @@ function App() {
enableMini={controls.miniViewer[0]}
aa={controls.aa[0]}
dpr={dpr}
- showFps={true}
+ showFps={controls.fps[0]}
/>
Date: Tue, 25 Feb 2020 17:48:40 +0000
Subject: [PATCH 24/38] fix callback dependencies
---
src/components/WebGLCanvas.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/WebGLCanvas.jsx b/src/components/WebGLCanvas.jsx
index c2b6a8e..41b6b9f 100644
--- a/src/components/WebGLCanvas.jsx
+++ b/src/components/WebGLCanvas.jsx
@@ -91,7 +91,7 @@ export default React.forwardRef(({mini = false, ...props}, ref) => {
// The 'state' will always be the initial value here
renderRequestRef.current = requestAnimationFrame(render);
- }, [gl, u, zoom, dpr, setFps]);
+ }, [gl, u, zoom, dpr, setFps, interval]);
// re-compile program if shader changes
useEffect(() => {
From 480a1812aa1be3a2b12586961b6aa95729b9b389 Mon Sep 17 00:00:00 2001
From: Joao Maio <10330349+JMaio@users.noreply.github.com>
Date: Tue, 25 Feb 2020 21:05:13 +0000
Subject: [PATCH 25/38] fps -> FPS
---
src/App.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/App.js b/src/App.js
index ad25fac..b96597a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -182,7 +182,7 @@ function App() {
/>
},
fps: {
- name: 'Show fps',
+ name: 'Show FPS',
ctrl:
Date: Wed, 26 Feb 2020 15:56:49 +0000
Subject: [PATCH 26/38] round pixel ratio to 3dp
---
public/clientDetect.js | 2 +-
src/App.js | 9 ++++++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/clientDetect.js b/public/clientDetect.js
index d8f5afc..8e079f0 100644
--- a/public/clientDetect.js
+++ b/public/clientDetect.js
@@ -195,7 +195,7 @@
mobile: mobile,
platform: navigator.platform,
screen: screenSize,
- dpr: window.devicePixelRatio,
+ dpr: +window.devicePixelRatio.toFixed(3),
gpu: renderer,
gpuVendor: gpuVendor,
userAgent: navigator.userAgent,
diff --git a/src/App.js b/src/App.js
index b96597a..350c20f 100644
--- a/src/App.js
+++ b/src/App.js
@@ -161,7 +161,14 @@ function App() {
placement: "top"
},
dpr: {
- name: `Use pixel ratio (${window.devicePixelRatio || 1})`,
+ // https://stackoverflow.com/a/12830454/9184658
+ // // There is a downside that values like 1.5 will give "1.50" as the output. A fix suggested by @minitech:
+ // var numb = 1.5;
+ // numb = +numb.toFixed(2);
+ // // Note the plus sign that drops any "extra" zeroes at the end.
+ // // It changes the result (which is a string) into a number again (think "0 + foo"),
+ // // which means that it uses only as many digits as necessary.
+ name: `Use pixel ratio (${+window.devicePixelRatio.toFixed(3) || 1})`,
ctrl:
Date: Wed, 26 Feb 2020 15:57:00 +0000
Subject: [PATCH 27/38] crosshair toggle
---
src/App.js | 12 +++++++++++-
src/components/MandelbrotRenderer.jsx | 9 ++++++---
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/App.js b/src/App.js
index 350c20f..409e57b 100644
--- a/src/App.js
+++ b/src/App.js
@@ -106,8 +106,9 @@ function App() {
let toggleInfo = () => setShowInfo(!showInfo);
let controls = {
- coords: useState(false),
miniViewer: useState(true),
+ crosshair: useState(true),
+ coords: useState(false),
maxI: useState(250),
aa: useState(false),
dpr: useState(false),
@@ -127,6 +128,14 @@ function App() {
onChange={() => toggleVal(controls.miniViewer)}
/>
},
+ crosshair: {
+ name: 'Crosshair',
+ ctrl: toggleVal(controls.crosshair)}
+ />
+ },
coords: {
name: 'Show coordinates',
ctrl:
Date: Wed, 26 Feb 2020 18:09:38 +0000
Subject: [PATCH 28/38] add favicons and theme
---
public/favicon-192.png | Bin 0 -> 60265 bytes
public/favicon-256.png | Bin 0 -> 99816 bytes
public/favicon-512.png | Bin 0 -> 289225 bytes
public/index.html | 4 ++--
public/manifest.json | 20 +++++++++++++-------
src/components/InfoDialog.jsx | 25 ++++++++++++++++---------
6 files changed, 31 insertions(+), 18 deletions(-)
create mode 100644 public/favicon-192.png
create mode 100644 public/favicon-256.png
create mode 100644 public/favicon-512.png
diff --git a/public/favicon-192.png b/public/favicon-192.png
new file mode 100644
index 0000000000000000000000000000000000000000..99cca91cb1978bdef2f36bf8983da82f66d1141d
GIT binary patch
literal 60265
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