From 5148646f649f1b9009ee53373f2d7d978b491213 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Fri, 8 Feb 2019 11:39:23 -0800 Subject: [PATCH] Initial "playground" for examples (#314) Initial "playground" for examples --- dune | 2 +- examples/Examples.re | 114 +++++++++++---- examples/dune | 2 +- examples/index.html | 8 ++ examples/stubs/ExampleStubs.re | 3 + examples/stubs/dune | 5 + examples/stubs/example_stubs.c | 14 ++ examples/stubs/example_stubs.js | 7 + playground/.gitignore | 3 + playground/README.md | 18 +++ playground/build.js | 83 +++++++++++ playground/package-lock.json | 247 ++++++++++++++++++++++++++++++++ playground/package.json | 26 ++++ playground/src/favicon.ico | Bin 0 -> 615 bytes playground/src/index.css | 106 ++++++++++++++ playground/src/index.html | 86 +++++++++++ 16 files changed, 696 insertions(+), 28 deletions(-) create mode 100644 examples/stubs/ExampleStubs.re create mode 100644 examples/stubs/dune create mode 100644 examples/stubs/example_stubs.c create mode 100644 examples/stubs/example_stubs.js create mode 100644 playground/.gitignore create mode 100644 playground/README.md create mode 100644 playground/build.js create mode 100644 playground/package-lock.json create mode 100644 playground/package.json create mode 100644 playground/src/favicon.ico create mode 100644 playground/src/index.css create mode 100644 playground/src/index.html diff --git a/dune b/dune index b8f2cb2ad..80e682aa0 100644 --- a/dune +++ b/dune @@ -1 +1 @@ -(ignored_subdirs (node_modules _esy)) +(ignored_subdirs (node_modules _esy playground)) diff --git a/examples/Examples.re b/examples/Examples.re index 8a40e58f4..5d64d7638 100644 --- a/examples/Examples.re +++ b/examples/Examples.re @@ -2,6 +2,8 @@ open Revery; open Revery.Core; /* open Revery.Math; */ +open ExampleStubs; + module SliderExample = Slider; module ScrollViewExample = ScrollView; @@ -16,6 +18,7 @@ let selectionHighlight = Color.hex("#90f7ff"); type example = { name: string, render: Window.t => React.syntheticElement, + source: string, }; type state = { @@ -25,35 +28,92 @@ type state = { let state: state = { examples: [ - {name: "Animation", render: _w => Hello.render()}, - {name: "Button", render: _ => DefaultButton.render()}, - {name: "Checkbox", render: _ => CheckboxExample.render()}, - {name: "Slider", render: _ => SliderExample.render()}, - {name: "Border", render: _ => Border.render()}, - {name: "ScrollView", render: _w => ScrollViewExample.render()}, - {name: "Calculator", render: w => Calculator.render(w)}, - {name: "Flexbox", render: _ => Flexbox.render()}, - {name: "Box Shadow", render: _ => Boxshadow.render()}, - {name: "Focus", render: _ => Focus.render()}, - {name: "Stopwatch", render: _ => Stopwatch.render()}, - {name: "Native", render: w => Native.render(w)}, - {name: "Input", render: w => InputExample.render(w)}, - {name: "Radio Button", render: _ => RadioButtonExample.render()}, - {name: "Game Of Life", render: _ => GameOfLife.render()}, - {name: "Screen Capture", render: w => ScreenCapture.render(w)}, - {name: "Tree View", render: w => TreeView.render(w)}, - {name: "Analog Clock", render: _w => AnalogClock.render()}, + {name: "Animation", render: _w => Hello.render(), source: "Hello.re"}, + { + name: "Button", + render: _ => DefaultButton.render(), + source: "DefaultButton.re", + }, + { + name: "Checkbox", + render: _ => CheckboxExample.render(), + source: "CheckboxExample.re", + }, + { + name: "Slider", + render: _ => SliderExample.render(), + source: "Slider.re", + }, + {name: "Border", render: _ => Border.render(), source: "Border.re"}, + { + name: "ScrollView", + render: _w => ScrollViewExample.render(), + source: "ScrollView.re", + }, + { + name: "Calculator", + render: w => Calculator.render(w), + source: "Calculator.re", + }, + {name: "Flexbox", render: _ => Flexbox.render(), source: "Flexbox.re"}, + { + name: "Box Shadow", + render: _ => Boxshadow.render(), + source: "Boxshadow.re", + }, + {name: "Focus", render: _ => Focus.render(), source: "Focus.re"}, + { + name: "Stopwatch", + render: _ => Stopwatch.render(), + source: "Stopwatch.re", + }, + {name: "Native", render: w => Native.render(w), source: "Native.re"}, + { + name: "Input", + render: w => InputExample.render(w), + source: "InputExample.re", + }, + { + name: "Radio Button", + render: _ => RadioButtonExample.render(), + source: "RadioButtonExample.re", + }, + { + name: "Game Of Life", + render: _ => GameOfLife.render(), + source: "GameOfLife.re", + }, + { + name: "Screen Capture", + render: w => ScreenCapture.render(w), + source: "ScreenCapture.re", + }, + { + name: "Tree View", + render: w => TreeView.render(w), + source: "TreeView.re", + }, + { + name: "Analog Clock", + render: _w => AnalogClock.render(), + source: "AnalogClock.re", + }, ], selectedExample: "Animation", }; +let getExampleByName = (state: state, example: string) => { + List.filter(x => String.equal(x.name, example), state.examples) |> List.hd; +}; + +let getSourceForSample = (state: state, example: string) => { + getExampleByName(state, example) |> (s => s.source); +}; + let noop = () => (); let getRenderFunctionSelector: (state, Window.t) => React.syntheticElement = - (s: state) => - List.filter(x => String.equal(x.name, s.selectedExample), state.examples) - |> List.hd - |> (a => a.render); + (s: state) => getExampleByName(s, s.selectedExample) |> (a => a.render); module ExampleButton = { let component = React.component("ExampleButton"); @@ -138,6 +198,8 @@ let init = app => { */ Animated.cancelAll(); + let sourceFile = getSourceForSample(s, x.name); + notifyExampleSwitched(sourceFile); App.dispatch(app, SelectExample(x.name)); }} />; @@ -190,12 +252,12 @@ let init = app => { if (Environment.webGL) { Window.maximize(win); + } else { + let xPosition = (dimensions.width - windowWidth) / 2; + let yPosition = (dimensions.height - windowHeight) / 2; + Window.setPos(win, xPosition, yPosition); }; - let xPosition = (dimensions.width - windowWidth) / 2; - let yPosition = (dimensions.height - windowHeight) / 2; - Window.setPos(win, xPosition, yPosition); - UI.start(win, render); }; diff --git a/examples/dune b/examples/dune index 8c5b4a1b3..8a15887d8 100644 --- a/examples/dune +++ b/examples/dune @@ -3,9 +3,9 @@ (preprocess (pps lwt_ppx)) (package Revery) (public_names Examples) - (js_of_ocaml) (libraries js_of_ocaml + ExampleStubs str Revery flex diff --git a/examples/index.html b/examples/index.html index b15f7f882..46e13dc75 100644 --- a/examples/index.html +++ b/examples/index.html @@ -17,6 +17,14 @@ // but it's not needed during runtime. window.require = false; + diff --git a/examples/stubs/ExampleStubs.re b/examples/stubs/ExampleStubs.re new file mode 100644 index 000000000..bd94bb676 --- /dev/null +++ b/examples/stubs/ExampleStubs.re @@ -0,0 +1,3 @@ +/* Notify external environments of switching tabs */ +external notifyExampleSwitched: string => unit = + "revery_example_notify_changed"; diff --git a/examples/stubs/dune b/examples/stubs/dune new file mode 100644 index 000000000..e9e9e52d5 --- /dev/null +++ b/examples/stubs/dune @@ -0,0 +1,5 @@ +(library + (name ExampleStubs) + (c_names example_stubs) + (js_of_ocaml (javascript_files example_stubs.js)) +) diff --git a/examples/stubs/example_stubs.c b/examples/stubs/example_stubs.c new file mode 100644 index 000000000..cec75d707 --- /dev/null +++ b/examples/stubs/example_stubs.c @@ -0,0 +1,14 @@ +#include + +#include +#include +#include +#include + +CAMLprim value revery_example_notify_changed(value vExample) { + CAMLparam1(vExample); + const char *szExampleSource = String_val(vExample); + + printf("Switched to example: %s\n", szExampleSource); + return Val_unit; +} diff --git a/examples/stubs/example_stubs.js b/examples/stubs/example_stubs.js new file mode 100644 index 000000000..fb86afdc6 --- /dev/null +++ b/examples/stubs/example_stubs.js @@ -0,0 +1,7 @@ +// Provides: revery_example_notify_changed +function revery_example_notify_changed(src) { + var window = joo_global_object.window; + if (window && window["__revery_playground_example_notify_changed"]) { + window["__revery_playground_example_notify_changed"](caml_to_js_string(src)); + } +} diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 000000000..aa9c5c6e6 --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,3 @@ +_build/ +host/ +sources/ diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 000000000..40df969a8 --- /dev/null +++ b/playground/README.md @@ -0,0 +1,18 @@ +# revery-playground + +### Examples w/ code alongside + +## Build + +### Prerequisites + +- Make sure you have run `esy build` from the root directory + +### Build + +- `npm install` +- `npm run build` + +### Testing + +- `npm start` diff --git a/playground/build.js b/playground/build.js new file mode 100644 index 000000000..679d4cb55 --- /dev/null +++ b/playground/build.js @@ -0,0 +1,83 @@ +// Simple build script to copy over files from the release folder + +let cp = require("child_process"); +let fs = require("fs-extra"); +let os = require("os"); +let path = require("path"); + +let playgroundRoot = __dirname +let playgroundSources = path.join(playgroundRoot, "src"); +let reveryRoot = path.join(playgroundRoot, ".."); +let playgroundBuild = path.join(playgroundRoot, "_build"); + +let nodeModulesSrc = path.join(playgroundRoot, "node_modules"); +let nodeModulesDest = path.join(playgroundBuild, "node_modules"); + +let playgroundExampleSources = path.join(playgroundRoot, "_build", "sources"); +let playgroundExampleHost = path.join(playgroundRoot, "_build", "host"); + +let reveryExampleSources = path.join(reveryRoot, "examples"); + +let getEsyPath = () => { + let result = cp.execSync("where esy"); + let found = result.toString("utf8"); + + let candidates = found.trim().split(os.EOL); + return candidates[candidates.length - 1]; +}; + +let getCommit = () => { + let result = cp.execSync("git rev-parse --short HEAD"); + return result.toString("utf8").trim(); +} + +let getVersion = () => { + let packageJson = fs.readFileSync(path.join(reveryRoot, "package.json")).toString("utf8"); + return JSON.parse(packageJson).version; +} + +let esyPath = getEsyPath(); +let commitId = getCommit(); +let version = getVersion(); +console.log("Esy path: " + esyPath); +console.log("Commit id: " + commitId); +console.log("Version: " + version); + +let getBuildArtifactFolder = () => { + let result = cp.spawnSync(esyPath, ["bash", "-c", "echo $cur__bin"], { cwd: reveryRoot }); + return result.stdout.toString("utf8").trim(); +}; + +let replace = (str, val, newVal) => { + return str.split(val).join(newVal); +}; + +let artifactFolder = getBuildArtifactFolder(); + +console.log("Artifact folder: " + artifactFolder); + +console.log(`Copying sources from ${playgroundSources} to ${playgroundBuild}...`); +fs.copySync(playgroundSources, playgroundBuild); +console.log("Sources copied."); + +console.log(`Copying examples from ${reveryExampleSources} to ${playgroundExampleSources}...`); +fs.copySync(reveryExampleSources, playgroundExampleSources); +console.log("Examples copied."); + +console.log("Copying artifacts..."); +fs.copySync(artifactFolder, playgroundExampleHost); +console.log("Artifacts copied."); + +console.log("Copying node_modules..."); +fs.copySync(nodeModulesSrc, nodeModulesDest); +console.log("node_modules copied."); + +console.log("Replacing constaints in index.html"); +let indexHtmlPath = path.join(playgroundBuild, "index.html"); + +let indexHtml = fs.readFileSync(indexHtmlPath).toString("utf8"); +indexHtml = replace(indexHtml, "{#VERSION}", version); +indexHtml = replace(indexHtml, "{#COMMIT}", commitId); + +fs.writeFileSync(indexHtmlPath, indexHtml); +console.log("Done!"); diff --git a/playground/package-lock.json b/playground/package-lock.json new file mode 100644 index 000000000..91ef157dd --- /dev/null +++ b/playground/package-lock.json @@ -0,0 +1,247 @@ +{ + "name": "revery-playground", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, + "corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ecstatic": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.0.tgz", + "integrity": "sha512-EblWYTd+wPIAMQ0U4oYJZ7QBypT9ZUIwpqli0bKDjeIIQnXDBK2dXtZ9yzRCOlkW1HkO8gn7/FxLK1yPIW17pw==", + "dev": true, + "requires": { + "he": "^1.1.1", + "mime": "^1.6.0", + "minimist": "^1.1.0", + "url-join": "^2.0.5" + } + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", + "dev": true + }, + "follow-redirects": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", + "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "dev": true, + "requires": { + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-server": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz", + "integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==", + "dev": true, + "requires": { + "colors": "1.0.3", + "corser": "~2.0.0", + "ecstatic": "^3.0.0", + "http-proxy": "^1.8.1", + "opener": "~1.4.0", + "optimist": "0.6.x", + "portfinder": "^1.0.13", + "union": "~0.4.3" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "monaco-editor": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.15.6.tgz", + "integrity": "sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "opener": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz", + "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "portfinder": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz", + "integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==", + "dev": true, + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "qs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", + "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "union": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz", + "integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=", + "dev": true, + "requires": { + "qs": "~2.3.3" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "url-join": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", + "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } +} diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 000000000..ce05341e6 --- /dev/null +++ b/playground/package.json @@ -0,0 +1,26 @@ +{ + "name": "revery-playground", + "version": "1.0.0", + "description": "Playground for Revery examples", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "node build.js", + "start": "http-server ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/revery-ui/revery.git" + }, + "author": "Bryan Phelps", + "license": "MIT", + "bugs": { + "url": "https://github.com/revery-ui/revery/issues" + }, + "homepage": "https://github.com/revery-ui/revery#readme", + "devDependencies": { + "fs-extra": "^7.0.1", + "http-server": "^0.11.1", + "monaco-editor": "^0.15.6" + } +} diff --git a/playground/src/favicon.ico b/playground/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0853d0fee78b2988a86bd690514810e63119a322 GIT binary patch literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DinK$vl=HlH+5!yiu<$B+!?w=*5RnH)uq|8Hw*dvU1hMt6mQ zz^4mK{U%3fVuJudympRMN#r|RmT=2Y!!%BqXo#L)Hj+w$2S;ceIk7b-TofCOq z;S4|9^S5!|%s|6Sx!?Y_w@|F7 zFZRj#rwVr(MNXaMc@-)A)=6;hUPV5y{6BLoP6e%)b8ca-=!FSW0%DjYxAf%Ni7VGJ z);ATGH$3|;@#xz8Pt&1QgcD4QXFPN4&|GKxajv4Efft`&Pe<@= z=|!4_TI(GZS{HvYlbU+)dPb;v#Ri=%N@4$NOr8}Ra#Sz8&mw=nd`Z}R$EYL4Ir$B< zl>dL^Noq{EdiTMFDx?20C5r^Oms~K=`Lc?y|7mA@!&ael(n8-IbYC>*{QIH)G^a)= z`);G3dHdhGDJ_~;kDQIH=Gs}tw|ucHyQuf_Jzti#En$;$cl18{Tyc_ROv=+++(L}s zuO8WG{V-7CW{8qtg3I>HMJrv@4(#9w$_#Woy+mlGLy6<&kW<{TA3RwlCbH^Q98?#N u2)p1hd&kN(+qi$(8(qy{L`GW~=5KCU?f2I4QZz7eFnGH9xvX + + + + + + + + + + + + + + + +
+
+
+ +
+
+ Revery Playground +
+
+ +
+
+
+
+
+ +
+
+ +
+ + + + + + + + + + +