on master help sign in
Merge pull request #381 from urbit/sigil-generator

Sigil generator

commitfe0b786ec1d3dd2f0a2fa1f9108ab119575f3102 authorFang <git@fang.io> dateFri 31 Jan 2020, 22:17 (7 years ago) parent1163a64756ed29
8 files changed+251−4
package-lock.json+5 package.json+1 src/form/ColorInput.js+50 src/lib/SigilDownloader.js+5 src/lib/router.js+2 src/lib/useSigilDownloader.js+58 src/views/Point.js+5−4 src/views/SigilGenerator.js+125
package-lock.json+5
@@ -9163,6 +9163,11 @@
91639163 "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
91649164 "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
91659165 },
9166+ "indigo-tokens": {
9167+ "version": "1.2.3",
9168+ "resolved": "https://registry.npmjs.org/indigo-tokens/-/indigo-tokens-1.2.3.tgz",
9169+ "integrity": "sha512-8FANdrmsgtTFzFFCMLncKAzmFmLxr/f9nR8smvotMk09JMsBRyQm+t5/95J8XUErc3u6q3XKseARLq3TVxidnA=="
9170+ },
91669171 "inflight": {
91679172 "version": "1.0.6",
91689173 "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
package.json+1
@@ -33,6 +33,7 @@
3333 "final-form-arrays": "^3.0.1",
3434 "final-form-set-field-data": "^1.0.2",
3535 "folktale": "^2.3.1",
36+ "indigo-tokens": "^1.2.3",
3637 "jszip": "^3.1.5",
3738 "keccak": "^1.4.0",
3839 "keythereum": "^1.0.4",
src/form/ColorInput.jsnew file+50
@@ -0,0 +1,50 @@
1+import React from 'react';
2+import { useField } from 'react-final-form';
3+import cn from 'classnames';
4+import { Flex } from 'indigo-react';
5+
6+const ColorInput = ({ name, colors, disabled = false, label, className }) => {
7+ const { input } = useField(name, { subscription: { value: true } });
8+
9+ const isWhite = c => c.toUpperCase() === '#FFFFFF';
10+
11+ return (
12+ <Flex col className={cn(className, 'flex-wrap mb1')}>
13+ <Flex.Item
14+ as="label"
15+ className={cn('f6 lh-tall', {
16+ black: !disabled,
17+ gray4: disabled,
18+ })}
19+ htmlFor={name}>
20+ {label}
21+ </Flex.Item>
22+ <Flex.Item as={Flex} wrap>
23+ {colors.map(c => (
24+ <Flex.Item
25+ as="button"
26+ type="button"
27+ key={c}
28+ className={cn('m0 p0 h7 w7 p2 flex flex-center', {
29+ 'b b-black': isWhite(c),
30+ })}
31+ style={{ backgroundColor: c }}
32+ onClick={() => input.onChange(c)}>
33+ {c === input.value ? (
34+ <div
35+ className={cn(
36+ { 'bg-black': isWhite(c), 'bg-white': !isWhite(c) },
37+ 'w2 h2 r-full'
38+ )}
39+ />
40+ ) : (
41+ <div />
42+ )}
43+ </Flex.Item>
44+ ))}
45+ </Flex.Item>
46+ </Flex>
47+ );
48+};
49+
50+export default ColorInput;
src/lib/SigilDownloader.js+5
@@ -14,6 +14,11 @@ export const initCanvas = (canvas, size) => {
1414 return canvas;
1515};
1616
17+export const cleanupCanvas = canvas => {
18+ canvas.width = 0;
19+ canvas.height = 0;
20+};
21+
1722export const loadImg = src =>
1823 new Promise((resolve, reject) => {
1924 const img = new Image();
src/lib/router.js+2
@@ -5,6 +5,7 @@ import Senate from 'views/Senate';
55import CreateGalaxy from 'views/CreateGalaxy';
66import IssueChild from 'views/IssueChild';
77import StarRelease from 'views/Release';
8+import SigilGenerator from 'views/SigilGenerator';
89
910import Point from 'views/Point';
1011import Points from 'views/Points';
@@ -36,4 +37,5 @@ export const ROUTES = {
3637 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
3738 [ROUTE_NAMES.INVITE_COHORT]: InviteCohort,
3839 [ROUTE_NAMES.STAR_RELEASE]: StarRelease,
40+ [ROUTE_NAMES.SIGIL_GENERATOR]: SigilGenerator,
3941};
src/lib/useSigilDownloader.jsnew file+58
@@ -0,0 +1,58 @@
1+import { useCallback } from 'react';
2+import { saveAs } from 'file-saver';
3+import { sigil, stringRenderer } from 'urbit-sigil-js';
4+import * as ob from 'urbit-ob';
5+
6+import {
7+ dataURItoBlob,
8+ loadImg,
9+ initCanvas,
10+ cleanupCanvas,
11+} from 'lib/SigilDownloader';
12+
13+export default function useSigilDownloader(canvasRef) {
14+ const downloadSigil = useCallback(
15+ (point, colors, size) => {
16+ const patp = ob.patp(point);
17+
18+ const _size = size / window.devicePixelRatio;
19+
20+ const DATA_URI_PREFIX = 'data:image/svg+xml;base64,';
21+
22+ // initialize canvas element
23+ const canvas = initCanvas(canvasRef.current, { x: _size, y: _size });
24+ const ctx = canvas.getContext('2d');
25+
26+ // make sigil svg and encoded into base64
27+ const svg = sigil({
28+ patp,
29+ renderer: stringRenderer,
30+ size: _size,
31+ colors,
32+ margin: (54 / 256) * _size,
33+ });
34+
35+ // FF rendering hack
36+ const svgDocument = new DOMParser().parseFromString(svg, 'image/svg+xml');
37+ svgDocument.documentElement.width.baseVal.valueAsString = `${size}px`;
38+ svgDocument.documentElement.height.baseVal.valueAsString = `${size}px`;
39+ const svgText = new XMLSerializer().serializeToString(svgDocument);
40+ const svg64 = btoa(svgText);
41+
42+ return loadImg(DATA_URI_PREFIX + svg64, _size, _size)
43+ .then(img => {
44+ ctx.drawImage(img, 0, 0, _size, _size);
45+ const png = dataURItoBlob(canvas.toDataURL('image/png'));
46+ saveAs(png, `${patp.slice(1)}-sigil.png`);
47+ ctx.clearRect(0, 0, _size, _size);
48+ })
49+ .catch(() => 'Error generating sigil')
50+ .then(r => {
51+ cleanupCanvas(canvasRef.current);
52+ return r;
53+ });
54+ },
55+ [canvasRef]
56+ );
57+ return { downloadSigil };
58+}
src/views/Point.js+5−4
@@ -13,7 +13,6 @@ import Passport from 'components/Passport';
1313import { ForwardButton, BootUrbitOSButton } from 'components/Buttons';
1414import CopyButton from 'components/CopyButton';
1515import Blinky, { matchBlinky } from 'components/Blinky';
16−import DownloadSigilButton from 'components/DownloadSigilButton';
1716import BarGraph from 'components/BarGraph';
1817import Chip from 'components/Chip';
1918import InviteSigilList from 'components/InviteSigilList';
@@ -72,7 +71,7 @@ export default function Point() {
7271
7372 const goSenate = useCallback(() => push(names.SENATE), [push, names]);
7473
75− const goInvite = useCallback(() => push(names.INVITE), [push, names]);
74+ const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
7675
7776 const goCohort = useCallback(() => push(names.INVITE_COHORT), [push, names]);
7877
@@ -217,6 +216,10 @@ export default function Point() {
217216 </Grid>
218217 <Grid className="pt2">
219218 {inviteButton}
219+ <Grid.Item full as={ForwardButton} onClick={goSigil}>
220+ Sigil
221+ </Grid.Item>
222+ <Grid.Divider />
220223 <Grid.Item
221224 full
222225 as={ForwardButton}
@@ -251,8 +254,6 @@ export default function Point() {
251254 Login Code
252255 </Grid.Item>
253256 <Grid.Divider />
254− <Grid.Item full as={DownloadSigilButton} point={point} />
255− <Grid.Divider />
256257 </Grid>
257258 </View>
258259 );
src/views/SigilGenerator.jsnew file+125
@@ -0,0 +1,125 @@
1+import React, { useRef, useCallback } from 'react';
2+import { Grid, Flex } from 'indigo-react';
3+import * as ob from 'urbit-ob';
4+import { FORM_ERROR } from 'final-form';
5+import { colors } from 'indigo-tokens';
6+
7+import { usePointCursor } from 'store/pointCursor';
8+
9+import View from 'components/View';
10+import Sigil from 'components/Sigil';
11+
12+import * as need from 'lib/need';
13+import { useLocalRouter } from 'lib/LocalRouter';
14+import useSigilDownloader from 'lib/useSigilDownloader';
15+
16+import { composeValidator, buildNumberValidator } from 'form/validators';
17+import BridgeForm from 'form/BridgeForm';
18+import FormError from 'form/FormError';
19+import SubmitButton from 'form/SubmitButton';
20+import { NumberInput } from 'form/Inputs';
21+import ColorInput from 'form/ColorInput';
22+
23+const BG_COLORS = [
24+ colors.black,
25+ colors.white,
26+ colors.blue,
27+ colors.blueLight,
28+ colors.blueDark,
29+ colors.redLight,
30+ colors.red,
31+ colors.redDark,
32+ colors.greenLight,
33+ colors.green,
34+ colors.greenDark,
35+ colors.yellowLight,
36+ colors.yellow,
37+ colors.yellowDark,
38+];
39+
40+const FG_COLORS = [colors.black, colors.white];
41+
42+export default function SigilGenerator() {
43+ const { pop } = useLocalRouter();
44+ const { pointCursor } = usePointCursor();
45+
46+ const validate = composeValidator({ size: buildNumberValidator(16) });
47+ const point = need.point(pointCursor);
48+
49+ const canvasRef = useRef(null);
50+
51+ const { downloadSigil } = useSigilDownloader(canvasRef);
52+ const onSubmit = useCallback(
53+ async (values, form) => {
54+ const error = await downloadSigil(point, values.colors, values.size);
55+ if (error) {
56+ return { [FORM_ERROR]: error };
57+ }
58+ // reset on next tick
59+ setTimeout(() => form.reset(values));
60+ },
61+ [point, downloadSigil]
62+ );
63+ return (
64+ <View pop={pop}>
65+ <BridgeForm
66+ validate={validate}
67+ initialValues={{
68+ size: 256,
69+ fgColor: '#FFFFFF',
70+ bgColor: '#000000',
71+ }}
72+ onSubmit={onSubmit}>
73+ {({ handleSubmit, values }) => (
74+ <Grid gap={6}>
75+ <Grid.Item full className="f7">
76+ Sigil
77+ </Grid.Item>
78+ <Grid.Item fourth={1}>
79+ <Sigil
80+ size={50}
81+ patp={ob.patp(point)}
82+ colors={[values.bgColor, values.fgColor]}
83+ />
84+ </Grid.Item>
85+
86+ <Grid.Item full as={Flex} col>
87+ <Flex.Item className="mb1">Urbit ID</Flex.Item>
88+ <Flex.Item className="mono">{ob.patp(point)}</Flex.Item>
89+ </Grid.Item>
90+ <Grid.Item
91+ fourth={1}
92+ as={NumberInput}
93+ name="size"
94+ label="Size (px)"
95+ />
96+ <Grid.Item
97+ full
98+ as={ColorInput}
99+ name="bgColor"
100+ label="Background Color"
101+ colors={BG_COLORS}
102+ />
103+ <Grid.Item
104+ full
105+ as={ColorInput}
106+ name="fgColor"
107+ label="Foreground Color"
108+ colors={FG_COLORS}
109+ />
110+ <Grid.Item
111+ full
112+ as={SubmitButton}
113+ handleSubmit={handleSubmit}
114+ accessory="↓">
115+ Download Sigil
116+ </Grid.Item>
117+ <Grid.Item full as={FormError} />
118+ </Grid>
119+ )}
120+ </BridgeForm>
121+
122+ <canvas style={{ display: 'none' }} ref={canvasRef} />
123+ </View>
124+ );
125+}