Browse Source

Rework of the app using Svelte

master
Mattéo Delabre 2 years ago
parent
commit
c367880ec8
Signed by: matteo GPG Key ID: AE3FBD02DC583ABB
  1. 44
      .eslintrc
  2. 2
      .gitignore
  3. 56
      CONTRIBUTING.fr.md
  4. 52
      CONTRIBUTING.md
  5. 121
      COPYING
  6. 19
      README.fr.md
  7. 18
      README.md
  8. 5941
      bundle.js
  9. BIN
      images/avatar.jpg
  10. BIN
      images/setting1.png
  11. BIN
      images/setting2.png
  12. BIN
      images/setting3.png
  13. BIN
      images/setting4.png
  14. 17
      index.html
  15. 2224
      package-lock.json
  16. 40
      package.json
  17. 16
      public/global.css
  18. 17
      public/index.html
  19. 42
      rollup.config.js
  20. 26
      scripts/app.js
  21. 48
      scripts/chaos.js
  22. 34
      scripts/controls.js
  23. 246
      scripts/fractal.js
  24. 41
      scripts/ifs.js
  25. 10
      scripts/index.js
  26. 224
      src/App.svelte
  27. 147
      src/Chaos.svelte
  28. 119
      src/Point.svelte
  29. 4
      src/main.js
  30. 27
      src/util.js
  31. 11
      styles/index.css

44
.eslintrc

@ -1,44 +0,0 @@
{
"extends": "eslint:recommended",
"rules": {
"no-shadow": 2,
"no-catch-shadow": 2,
"no-shadow-restricted-names": 2,
"radix": 2,
"wrap-iife": 2,
"yoda": 2,
"semi": 2,
"indent": 2,
"camelcase": 2,
"brace-style": 2,
"comma-spacing": 2,
"comma-style": 2,
"quotes": [2, "single", "avoid-escape"],
"no-spaced-func": 2,
"space-after-keywords": 2,
"space-before-blocks": 2,
"space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
"space-in-parens": 2,
"space-infix-ops": 2,
"space-return-throw-case": 2,
"space-unary-ops": 2,
"no-trailing-spaces": 0,
"no-underscore-dangle": 0
},
"ecmaFeatures": {
"modules": true,
"jsx": true
},
"plugins": [
"react"
],
"env": {
"es6": true,
"browser": true
}
}

2
.gitignore

@ -1 +1,3 @@
*.map
node_modules
public/bundle.*

56
CONTRIBUTING.fr.md

@ -1,56 +0,0 @@
# Contribuer
[Looking for the english version?](CONTRIBUTING.md)
Merci de votre intérêt à contribuer à ce code !
Toutes les contributions (même les plus petites) sont les bienvenues.
Pour garder une certaine cohérence dans le code, merci de suivre
ces quelques règles.
## 1. Commit tags
Tous les commits doivent être précédés d'emojis dans la mesure
du possible pour que la liste des commits soit plus lisible.
| Emoji | Type de commit |
|:----------:|:---------------------------------|
| :book: | Changement dans la documentation |
| :bug: | Correction de bug |
| :ledger: | Déplacement de fichiers |
| :bulb: | Nouvelles fonctionnalités |
| :lipstick: | Correction du style de code |
## 2. Branches
Merci d'utiliser un nom de branche différent de
`master` pour vos pull requests, pour que l'historique
soit plus lisible.
Par exemple, pour améliorer la documentation, vous
pourriez choisir le nom `improve-docs`.
## 3. Conventions de style
Le code Javascript peut être écrit suivant
[beaucoup](https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml)
[de](https://github.com/airbnb/javascript)
[styles](https://github.com/felixge/node-style-guide)
[différents](https://contribute.jquery.org/style-guide/js/)
mais nous ne sommes pas aussi durs que ceux-ci.
La règle principale est d'utiliser [ESLint](http://eslint.org/) pour
vérifier si votre code s'accorde avec nos conventions de style.
Voici quelques unes des règles :
* utiliser le paramètre de base dans `parseInt()`;
* utiliser le *one true brace style;*
* mettre un espace après les virgules, pas d'espace avant;
* mettre les virgules à la fin des lignes de préférence;
* utiliser des guillemets simples;
* écrire en camelcase;
* utiliser 4 espaces pour l'indentation.
## 4. Langue
De préférence, les noms de variables, fonctions, le texte des commentaires,
les descriptions de commits doivent être écrits en *anglais.*

52
CONTRIBUTING.md

@ -1,52 +0,0 @@
# Contributing
[Voir ceci en français](CONTRIBUTING.fr.md)
Thank you for your interest in contributing to this repo!
All contributions (even small ones) are welcome.
In order to keep this repo consistent, please
try to follow these rules.
## 1. Commit tags
All commits should be tagged with emojis whenever possible
to make the commit list more readable.
| Emoji | Commit content |
|:----------:|:--------------------- |
| :book: | Documentation updates |
| :bug: | Bug fixes |
| :ledger: | Moving files |
| :bulb: | New features |
| :lipstick: | Fixing coding style |
## 2. Branches
Please use a branch name that differs from `master`
when making pull requests, so that the network
history is more readable.
For example, if you wanted to fix the issue
"improve documentation", you could have
chosen the following branch name: `improve-docs`.
## 3. Coding style
Javascript can be authored by following
[a](https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml)
[lot](https://github.com/airbnb/javascript)
[of](https://github.com/felixge/node-style-guide)
[different](https://contribute.jquery.org/style-guide/js/)
[style guides](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Coding_Style)
but we decided to be a bit soft on that.
As a rule of thumb, use [ESLint](http://eslint.org/) to check if your code complies
with our style conventions. Here are some of the rules:
* use the radix parameter in `parseInt()` calls;
* use the *one true brace style;*
* put one space after commas, and no space before;
* put your comma at the end of the lines;
* use simple quotes;
* use camelcase;
* use 4 spaces for indentation.

121
COPYING

@ -1,121 +0,0 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

19
README.fr.md

@ -1,19 +0,0 @@
# Chaos
[Looking for the english version?](README.md)
Créer des fractales avec le jeu du chaos.
== À FAIRE ==
## Contribuer
Toutes les informations sont dans le
[guide du contributeur.](https://github.com/matteodelabre/chaos/blob/master/CONTRIBUTING.fr.md)
## Licence
Chaos ― Créer des fractales avec le jeu du chaos.
Écrit en 2013 ― 2015 par Mattéo Delabre ([bonjour@matteodelabre.me](mailto:bonjour@matteodelabre.me)).
Dans la mesure permise par la loi, l'auteur dédie mondialement tous ses droits d'auteur et droits voisins sur ce logiciel au **domaine public.** Ce logiciel est distribué sans aucune garantie.
Vous devriez avoir reçu une copie du *CC0 Domain Dedication* avec ce logiciel. Sinon, voir http://creativecommons.org/publicdomain/zero/1.0/.

18
README.md

@ -1,18 +0,0 @@
# Chaos
[Voir ceci en français](README.fr.md)
Creating fractals with the chaos game.
== TODO ==
## Contributing
Check out the [contribution guide.](https://github.com/matteodelabre/chaos/blob/master/CONTRIBUTING.md)
## License
Chaos ― Creating fractals with the chaos game.
Written in 2013 ― 2015 by Mattéo Delabre ([bonjour@matteodelabre.me](mailto:bonjour@matteodelabre.me)).
To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the **public domain** worldwide. This software is distributed without any warranty.
You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see http://creativecommons.org/publicdomain/zero/1.0/.

5941
bundle.js

File diff suppressed because one or more lines are too long

BIN
images/avatar.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

BIN
images/setting1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/setting2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

BIN
images/setting3.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/setting4.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

17
index.html

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Chaos game</title>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700,400italic|Playfair+Display:400,400italic,700" rel="stylesheet">
<link href="https://matteodelabre.me/styles/common.css" rel="stylesheet">
<link href="https://matteodelabre.me/styles/demos.css" rel="stylesheet">
<link href="styles/index.css" rel="stylesheet">
</head>
<body>
<div id="react"></div>
<script src="bundle.js"></script>
</body>
</html>

2224
package-lock.json

File diff suppressed because it is too large

40
package.json

@ -1,33 +1,23 @@
{
"name": "chaos",
"version": "1.0.0",
"description": "Plotting fractals with the chaos game",
"repository": {
"type": "git",
"url": "git+https://github.com/matteodelabre/chaos.git"
"version": "2.0.0",
"description": "Exploring the chaos game",
"license": "CC0",
"private": true,
"devDependencies": {
"rollup": "^1.17.0",
"rollup-plugin-commonjs": "^10.0.1",
"rollup-plugin-livereload": "^1.0.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-svelte": "^5.1.0",
"rollup-plugin-terser": "^5.1.1",
"svelte": "^3.6.9"
},
"scripts": {
"build": "browserify -t [ babelify --presets [ es2015 react ] ] scripts/index.js | babel --presets es2015 > bundle.js"
"build": "rollup -c",
"autobuild": "rollup -c -w"
},
"keywords": [
"chaos",
"fractals",
"game"
],
"author": "Mattéo Delabre",
"license": "CC0-1.0",
"bugs": {
"url": "https://github.com/matteodelabre/chaos/issues"
},
"homepage": "https://github.com/matteodelabre/chaos#readme",
"dependencies": {
"babel-cli": "^6.3.17",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babelify": "^7.2.0",
"browserify": "^12.0.1",
"react": "^0.14.3",
"react-dom": "^0.14.3",
"the-dom": "^0.1.0"
"ml-matrix": "^6.2.0"
}
}

16
public/global.css

@ -0,0 +1,16 @@
body, html
{
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
outline: 0;
--easing: cubic-bezier(0.4, 0.0, 0.2, 1);
}
*, *::before, *::after
{
box-sizing: border-box;
}

17
public/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<meta name="viewport" content="width=device-width">
<title>Chaos game</title>
<link rel="stylesheet" href="/global.css">
<link rel="stylesheet" href="/bundle.css">
<script defer src="/bundle.js"></script>
</head>
<body>
</body>
</html>

42
rollup.config.js

@ -0,0 +1,42 @@
import svelte from 'rollup-plugin-svelte';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/bundle.js'
},
plugins: [
svelte({
dev: !production,
css: css => css.write('public/bundle.css')
}),
resolve({
browser: true,
dedupe: importee => importee === 'svelte'
|| importee.startsWith('svelte/')
}),
commonjs(),
// In development mode, watch the `public` directory
// and refresh the browser on changes
!production && livereload('public'),
// In production, minify
production && terser()
],
watch: {
chokidar: false,
clearScreen: false
}
};

26
scripts/app.js

@ -1,26 +0,0 @@
'use strict';
import * as React from 'react';
import { Controls } from './controls';
import { Fractal } from './fractal';
import { sierpinski } from './ifs';
/**
* Render the app sidebar and fractal display
*/
export class App extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
return <div className="split">
<Controls />
<Fractal system={sierpinski} />
</div>;
}
}

48
scripts/chaos.js

@ -1,48 +0,0 @@
'use strict';
/**
* Choose an index at random among a list of weights,
* more weighted indices have a greater proability to be chosen
*
* @param {Array} weights List of weights
* @return {number} Selected index
*/
const chooseIndex = weights => {
const number = Math.random();
let sum = 0, index = 0;
while (number >= sum) {
sum += weights[index];
index += 1;
}
return index - 1;
};
/**
* Starting from `point`, generate `iterations` points
* by applying randomly-chosen transformations
*
* @param {Array} point Starting point
* @param {number} iterations Number of points to plot
* @param {Array} transforms List of available transforms
* @param {Array} weights Probability weights for each transform
* @return {Array} Generated points
*/
export const applyChaos = (point, iterations, transforms, weights) => {
const points = [];
if (weights === undefined) {
weights = Array.apply(null, Array(transforms.length)).map(
() => 1 / transforms.length
);
}
while (iterations--) {
const index = chooseIndex(weights);
point = transforms[index](point);
points.push(point);
}
return points;
};

34
scripts/controls.js

@ -1,34 +0,0 @@
'use strict';
import * as React from 'react';
/**
* Render app controls
*/
export class Controls extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
return <aside>
<header>
<a href="/">
<img src="images/avatar.jpg" alt="Photo de Mattéo" />
<span>Mattéo Delabre </span><br />
Back to home
</a>
</header>
<h1>The Chaos Game</h1>
<h3>Creating fractals with the chaos game</h3>
Sommets <input id="vertices" type="range" min="3" max="12" step="1" defaultValue="3" /><br />
Fraction 1/<input id="fraction" type="range" min="1" max="6" step="0.01" defaultValue="2" />
</aside>;
}
}

246
scripts/fractal.js

@ -1,246 +0,0 @@
'use strict';
import * as React from 'react';
import { applyChaos } from './chaos';
/**
* Render a canvas element that draws a fractal out of a
* given iterated function system
*/
class Fractal extends React.Component {
constructor(props) {
super(props);
this.state = {
zoom: 30,
dragging: false,
center: null,
points: null
};
}
/**
* Adjust zooming level and center so that wheeling the mouse
* on given point zooms around it
*
* @param {WheelEvent} event Mouse event
* @return {null}
*/
wheel(event) {
const zoom = this.state.zoom;
const center = this.state.center;
const height = this.ctx.canvas.height;
const delta = event.deltaMode === 0 ? event.deltaY / 53 : event.deltaY;
const newZoom = zoom * Math.max(0, 1 - delta * .035);
// which (unprojected) point does the mouse point on?
const mouse = [
(event.nativeEvent.offsetX - center[0]) / zoom,
(height - event.nativeEvent.offsetY - center[1]) / zoom
];
// we need to set the center so that `mouse` stays at
// the same position on screen, i.e (vectorially):
// mouse * newZoom + newCenter = mouse * zoom + center
// => newCenter = mouse * zoom - mouse * newZoom + center
this.setState({
zoom: newZoom,
center: [
mouse[0] * zoom - mouse[0] * newZoom + center[0],
mouse[1] * zoom - mouse[1] * newZoom + center[1]
]
}, this.draw);
event.preventDefault();
}
/**
* Save the mouse coordinates when we click down,
* for use by `mouseMove()`
*
* @param {MouseEvent} event Mouse event
* @return {null}
*/
mouseDown(event) {
this.setState({
dragging: [
event.nativeEvent.offsetX,
event.nativeEvent.offsetY
]
});
}
/**
* Save the fact that the mouse was released
*
* @return {null}
*/
mouseUp() {
this.setState({
dragging: false
});
}
/**
* When moving the mouse while clicking, pan the figure
*
* @param {MouseEvent} event Mouse event
* @return {null}
*/
mouseMove(event) {
const dragging = this.state.dragging;
const center = this.state.center;
if (dragging !== false) {
const newMouse = [
event.nativeEvent.offsetX,
event.nativeEvent.offsetY
];
const movement = [
newMouse[0] - dragging[0],
newMouse[1] - dragging[1]
];
// move the center by given offset and redraw
this.setState({
dragging: newMouse,
center: [
center[0] + movement[0],
center[1] - movement[1]
]
}, this.draw);
event.preventDefault();
}
}
/**
* Redraw `points` on the canvas, scaled with given
* `zoom` and based on current `center`
*
* @return {null}
*/
draw() {
const width = this.ctx.canvas.width;
const height = this.ctx.canvas.height;
const zoom = this.state.zoom;
const center = this.state.center;
const points = this.state.points;
// do not plot (very) small sizes
if (width < 1) {
return;
}
this.ctx.clearRect(0, 0, width, height);
// fill each point in `points` skipping the first 50 ones
const image = this.ctx.getImageData(0, 0, width, height);
const length = points.length;
const color = [0, 0, 0];
for (let i = 50; i < length; i += 1) {
const x = Math.floor(points[i][0] * zoom + center[0]);
const y = height - Math.floor(points[i][1] * zoom + center[1]);
if (x >= 0 && x < width && y >= 0 && y < height) {
const index = (y * width + x) * 4;
image.data[index] = color[0];
image.data[index + 1] = color[1];
image.data[index + 2] = color[2];
image.data[index + 3] = 255;
}
}
this.ctx.putImageData(image, 0, 0);
}
/**
* Get the container size
*
* @return {Array} Width and height of the container
*/
getSize() {
return [this.container.clientWidth, this.container.clientHeight];
}
/**
* Update the canvas size to its parent size
* and redraw
*
* @return {null}
*/
resize() {
[this.ctx.canvas.width, this.ctx.canvas.height] = this.getSize();
this.draw();
}
/**
* Calculate points with current system
*
* @return {Array} Points to be drawn
*/
calculate() {
return applyChaos(
[0, 0],
this.props.iterations,
...this.props.system
);
}
/**
* Setup resize listener and make initial drawing
* when the component has been mounted
*
* @return {null}
*/
componentDidMount() {
window.addEventListener('resize', this.resize.bind(this));
this.setState({
center: this.getSize().map(x => Math.floor(x / 2)),
points: this.calculate()
}, this.resize.bind(this));
}
/**
* Remove the resize listener before unmounting the component
*
* @return {null}
*/
componentWillUnmount() {
window.removeEventListener('resize', this.resize.bind(this));
}
/**
* Never create a new canvas
*/
shouldComponentUpdate() {
return false;
}
/**
* Create a canvas with correct listeners
*/
render() {
return <div id="content" ref={div => this.container = div}>
<canvas
ref={canvas => this.ctx = canvas.getContext('2d')}
onWheel={this.wheel.bind(this)}
onMouseDown={this.mouseDown.bind(this)}
onMouseUp={this.mouseUp.bind(this)}
onMouseMove={this.mouseMove.bind(this)}
></canvas>
</div>;
}
}
Fractal.defaultProps = {
iterations: 200000,
system: [[], []]
};
export { Fractal };

41
scripts/ifs.js

@ -1,41 +0,0 @@
'use strict';
const linearTransform = (a, b, c, d, e, f) => point => [
a * point[0] + b * point[1] + e,
c * point[0] + d * point[1] + f
];
const polygonTransforms = (vertices, frac) => vertices.map(
vertex => linearTransform(
frac, 0, 0, frac,
vertex[0] * (frac - 1), vertex[1] * (frac - 1)
)
);
const regularVertices = count => {
var step = 2 * Math.PI / count;
var initial = -Math.atan(Math.sin(step) / (Math.cos(step) - 1));
var result = [];
for (var i = 0; i < count; i += 1) {
var current = step * i + initial;
result.push([Math.cos(current) * 10, Math.sin(current) * 10]);
}
return result;
};
export const barnsley = [[
linearTransform(0, 0, 0, 0.16, 0, 0),
linearTransform(.85, .04, -.04, .85, 0, 1.6),
linearTransform(.20, -.26, .23, .22, 0, 1.6),
linearTransform(-.15, .28, .26, .24, 0, .44)
], [
.01, .85, .07, .07
]];
export const sierpinski = [
polygonTransforms(regularVertices(3), 1 / 2),
[1 / 3, 1 / 3, 1 / 3]
];

10
scripts/index.js

@ -1,10 +0,0 @@
'use strict';
import { App } from './app';
import * as React from 'react'; // eslint-disable-line no-unused-vars
import { render } from 'react-dom';
render(
<App />,
document.querySelector('#react')
);

224
src/App.svelte

@ -0,0 +1,224 @@
<script>
import { onMount } from 'svelte';
import { writable, derived } from 'svelte/store';
import { Matrix } from 'ml-matrix';
import Chaos from './Chaos.svelte';
import Point from './Point.svelte';
// Page dimensions
let pageWidth;
let pageHeight;
// Controls dimensions
const controlsWidth = 200;
// Render dimensions
$: renderWidth = pageWidth - controlsWidth;
$: renderHeight = pageHeight;
// Number of points
let count = 3;
// Whether controls are shown
let controls = true;
// Fraction
const ratios = writable([]);
// Bounding points of the fractal
const points = writable([]);
// Center of the fractal
const center = derived(points, ($points, set) =>
{
set(Matrix.div(
$points.reduce(
(prev, point) => Matrix.add(prev, point),
Matrix.zeros(3, 1)
),
$points.length
));
});
// Derived IFS
const ifs = derived([ratios, points], ([$ratios, $points], set) =>
{
set($points.map((point, index) => [
1,
new Matrix([
[$ratios[index], 0, point.get(0, 0) * (1 - $ratios[index])],
[0, $ratios[index], point.get(1, 0) * (1 - $ratios[index])],
[0, 0, 1]
])
]))
});
const myifs = [
[1, new Matrix([
[0, 0, 0],
[0, 0.16, 0],
[0, 0, 1]
])],
[85, new Matrix([
[0.85, 0.04, 0],
[-0.04, 0.85, 160],
[0, 0, 1]
])],
[7, new Matrix([
[0.20, -0.26, 0],
[0.23, 0.22, 160],
[0, 0, 1]
])],
[7, new Matrix([
[-0.15, 0.28, 0],
[0.26, 0.24, 44],
[0, 0, 1]
])]
];
for (let f of myifs)
{
/* f[1] = new Matrix([ */
/* [1, 0, 100], */
/* [0, 1, 100], */
/* [0, 0, 1] */
/* ]).mmul(f[1]); */
}
console.log(myifs);
$: {
// Generate a regular polygon with the given number of vertices
// and half ratio for each vertex
const pageCenter = [renderWidth / 2, renderHeight / 2];
const radius = Math.min(renderWidth, renderHeight) * 0.4;
points.set(
Array.from({length: count}).map(
(_, index) => index * 2 * Math.PI / count - Math.PI / 2
).map(angle => Matrix.columnVector([
pageCenter[0] + Math.cos(angle) * radius,
pageCenter[1] + Math.sin(angle) * radius,
1
]))
);
ratios.set(
Array.from({length: count}).fill(0.5)
);
}
</script>
<style>
.app
{
display: flex;
width: 100%;
height: 100%;
}
.controls
{
flex: 1;
}
.render
{
flex: 1;
position: relative;
}
.render .overlay
{
position: absolute;
top: 0;
left: 0;
}
</style>
<div class="app" bind:clientWidth={pageWidth} bind:clientHeight={pageHeight}>
<aside class="controls" style="width: {controlsWidth}px">
<p>
<button on:click={() => ++count}>Add point</button>
<button on:click={() => --count}>Remove point</button>
</p>
<p>
{#if controls}
Controls visible.
<button on:click={() => controls = false}>Hide</button>
{:else}
Controls hidden.
<button on:click={() => controls = true}>Show</button>
{/if}
</p>
</aside>
<main class="render" style="width: {renderWidth}px">
<Chaos
width={renderWidth} height={renderHeight}
start={$center} ifs={$ifs}
/>
{#if controls}
<svg
class="overlay"
width={renderWidth} height={renderHeight}
viewBox="0 0 {renderWidth} {renderHeight}"
>
{#each $points as point}
<line
stroke="#333"
x1={$center.get(0, 0)}
y1={$center.get(1, 0)}
x2={point.get(0, 0)}
y2={point.get(1, 0)}
/>
{/each}
</svg>
{#each $points as point, index}
<Point
x={point.get(0, 0)}
y={point.get(1, 0)}
on:move={evt => points.set([
...$points.slice(0, index),
Matrix.columnVector([evt.detail.x, evt.detail.y, 1]),
...$points.slice(index + 1)
])}
/>
{/each}
{#each $ifs as current, index}
<Point
x={current[1].mmul($center).get(0, 0)}
y={current[1].mmul($center).get(1, 0)}
on:move={evt => {
const point = Matrix.columnVector([evt.detail.x, evt.detail.y, 1]);
const vec1 = Matrix.sub($points[index], $center);
const vec2 = Matrix.sub(point, $center);
const nextRatio = 1 - vec1.dot(vec2) / vec1.dot(vec1);
if (evt.detail.shift)
{
ratios.set(
Array.from({length: $ratios.length}).fill(nextRatio)
);
}
else
{
ratios.set([
...$ratios.slice(0, index),
nextRatio,
...$ratios.slice(index + 1)
]);
}
}}
/>
{/each}
{/if}
</main>
</div>

147
src/Chaos.svelte

@ -0,0 +1,147 @@
<!--
Simulate a chaos game on an iterated function set (IFS)
and plot the result on a canvas of given dimensions.
-->
<script>
import { onMount, onDestroy, tick, afterUpdate } from 'svelte';
import { Matrix } from 'ml-matrix';
import { choose } from './util.js';
/** Width of the canvas on which the chaos figure is rendered. */
export let width = 500;
/** Height of the canvas on which the chaos figure is rendered. */
export let height = 500;
/** Total number of iterations to perform. */
export let totalIterations = 100000;
/** Number of iterations to perform in one frame. */
export let iterationsPerFrame = 1000;
/**
* Iterated function set to use for rendering the figure as a list of
* pairs, each containing the weight of the function and the function
* as a 3×3 matrix.
*/
export let ifs = [[1, Matrix.identity(3)]];
/** Initial point for the iteration, in homogeneous coordinates. */
export let start = Matrix.columnVector([0, 0, 1]);
// Canvas and drawing context on which the figure is rendered
let canvas;
let ctx;
// Handle to the next scheduled frame
let nextFrame = null;
/**
* Perform random chaos iteration on the given point, leaving traces
* behind as the iteration goes.
*
* @param point Starting point for the iteration.
* @param ifs Iterated function set to use.
* @param iterations Number of iterations to perform.
*/
const randomIterate = (point, ifs, iterations) =>
{
// Only perform a limited amount of iterations and
// defer the remaining ones to the next frame
for (let i = 0; i < iterationsPerFrame; ++i)
{
const [_, matrix] = choose(ifs);
point = matrix.mmul(point);
const x = Math.floor(point.get(0, 0));
const y = Math.floor(point.get(1, 0));
if (x >= 0 && x < width && y >= 0 && y < height)
{
ctx.fillRect(x, y, 1, 1);
}
}
if (iterations > iterationsPerFrame)
{
nextFrame = window.requestAnimationFrame(() => randomIterate(
point, ifs,
iterations - iterationsPerFrame
));
}
};
/**
* Perform deterministic chaos iteration on each of the given points,
* leaving traces behind as the iteration goes.
*
* @param points Seed points, modified in-place.
* @param index Index from which to iterate in the points array.
* @param ifs Iterated function set to use.
* @param iterations Number of iterations to perform.
*/
const deterministicIterate = (points, index, ifs, iterations) =>
{
let i = index;
for (
let frameIterations = 0;
frameIterations < iterationsPerFrame;
frameIterations += ifs.length,
i = (i + ifs.length) % points.length
)
{
points.splice(
i, 1,
...ifs.map(([_, matrix]) =>
{
const nextPoint = matrix.mmul(points[i]);
const x = Math.floor(nextPoint.get(0, 0));
const y = Math.floor(nextPoint.get(1, 0));
if (x >= 0 && x < width && y >= 0 && y < height)
{
ctx.fillRect(x, y, 1, 1);
}
return nextPoint;
})
);
}
if (iterations > iterationsPerFrame)
{
nextFrame = window.requestAnimationFrame(() => deterministicIterate(
points, i, ifs,
iterations - iterationsPerFrame
));
}
};
/** Interrupt the iteration process. */
const stopLoop = () =>
{
window.cancelAnimationFrame(nextFrame);
};
$: {
stopLoop();
if (ctx && ifs.length)
{
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'black';
deterministicIterate([start], 0, ifs, totalIterations);
}
}
onMount(() => ctx = canvas.getContext('2d'));
onDestroy(() => stopLoop());
</script>
<canvas
bind:this={canvas}
draggable=false
width={width} height={height}
/>

119
src/Point.svelte

@ -0,0 +1,119 @@
<!--
Represent a point on the screen that can optionally be moved by the user.
-->
<script>
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
/** Horizontal position of the point. */
export let x = 0;
/** Vertical position of the point. */
export let y = 0;
/** Radius of the circle that represents the point. */
export let size = 15;
/** Whether the user is allowed to move the point. */
export let movable = true;
// Reference to the <div> element that represents the point
let element = null;
// Bounds of the parent node, computed only once when the user
// starts dragging
let bounds;
// Whether the user is currently dragging the point
let dragging = false;
let dispatch = createEventDispatcher();
const start = evt =>
{
if (!movable) return;
bounds = element.parentNode.getBoundingClientRect();
dragging = true;
window.addEventListener('mousemove', move);
window.addEventListener('touchmove', move);
window.addEventListener('mouseup', end);
window.addEventListener('touchend', end);
};
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
const move = evt =>
{
const [userX, userY] = evt.changedTouches
? [evt.changedTouches[0].clientX, evt.changedTouches[0].clientY]
: [evt.clientX, evt.clientY];
dispatch('move', {
x: clamp(userX - bounds.left, 0, bounds.width),
y: clamp(userY - bounds.top, 0, bounds.height),
shift: evt.shiftKey
});
};
const end = evt =>
{
dragging = false;
window.removeEventListener('mousemove', move);
window.removeEventListener('touchmove', move);
window.removeEventListener('mouseup', end);
window.removeEventListener('touchend', end);
};
onMount(() =>
{
element.addEventListener('mousedown', start);
element.addEventListener('touchstart', start);
});
onDestroy(() =>
{
element.removeEventListener('mousedown', start);
element.removeEventListener('touchstart', start);
window.removeEventListener('mousemove', move);
window.removeEventListener('touchmove', move);
window.removeEventListener('mouseup', end);
window.removeEventListener('touchend', end);
});
</script>
<style>
div
{
display: block;
position: absolute;
border-radius: 100%;
background: #333;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
transition:
box-shadow .15s var(--easing),
transform .15s var(--easing),
opacity .15s var(--easing);
}
div.movable
{
cursor: pointer;
}
div.movable.dragging
{
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2);
transform: scale(1.5);
}
</style>
<div
style="width: {size}px; height: {size}px; left: {x - size / 2}px; top: {y - size / 2}px;"
class:dragging
class:movable
draggable="false"
bind:this={element}
/>

4
src/main.js

@ -0,0 +1,4 @@
import App from './App.svelte';
const app = new App({target: document.body});
export default app;

27
src/util.js

@ -0,0 +1,27 @@
/**
* Choose a pair at random among a list of weighted pairs.
*
* @param pairs List of weighted pairs, with the weight coming first.
* @return Selected pair, or null if there is nothing to choose from.
*/
export const choose = pairs =>
{
const total = pairs.reduce((prev, [weight, _]) => prev + weight, 0);
if (total === 0)
{
return null;
}
const value = Math.random() * total;
let sum = 0;
let index = 0;
while (value >= sum)
{
sum += pairs[index][0];
index += 1;
}
return pairs[index - 1];
};

11
styles/index.css

@ -1,11 +0,0 @@
/**
* Styles for chaos game
*/
#react {
height: 100%;
}
#content {
overflow: hidden;
}
Loading…
Cancel
Save