on master help sign in
incorporate new card designs
commitabd024f65f96c6e6bcc33a1aa153bfa9bb52503e authorLiam Fitzgerald <hastuc-dibtux@gmail.com> dateMon 7 Oct 2019, 21:54 (7 years ago) parentda154dd
4 files changed+472−144
public/spritesheet_a.png src/components/Passport.js+272−140 src/lib/card.js+191 src/views/Point.js+9−4
public/spritesheet_a.pngnew file

Binary file not shown.

src/components/Passport.js+272−140
@@ -1,36 +1,51 @@
1−import React, { useMemo } from 'react';
2−import cn from 'classnames';
1+import React from 'react';
32import { Just, Nothing } from 'folktale/maybe';
43import ob from 'urbit-ob';
5−import { Grid, Flex, Text } from 'indigo-react';
6−import { times } from 'lodash';
4+import BN from 'bn.js';
75
86import * as need from 'lib/need';
9−import usePointBirthday from 'lib/usePointBirthday';
10−
11−import Blinky, {
12− LOADING_CHARACTER,
13− INTERSTITIAL_CHARACTER,
14− matchBlinkyDate,
15−} from './Blinky';
16−import Sigil from './Sigil';
17−import MaybeSigil from './MaybeSigil';
7+import { chunkStr, Matrix, walk, rand } from 'lib/card';
188import usePermissionsForPoint from 'lib/usePermissionsForPoint';
19−import { useWallet } from 'store/wallet';
209import { useSyncOwnedPoints } from 'lib/useSyncPoints';
2110
22−const TICKET_CHAR_LENGTH = 30;
23−const TICKET = times(TICKET_CHAR_LENGTH, i =>
24− i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
25−).join('');
26−const PENDING_TICKET = TICKET;
27−const VALID_TICKET = `${TICKET} ◆`;
11+import { useWallet } from 'store/wallet';
2812
29−const VISIBLE_ADDRESS_CHAR_COUNT = 20;
30−const EMPTY_ADDRESS = times(VISIBLE_ADDRESS_CHAR_COUNT, () => '<').join('');
31−const ADDRESS_BUFFER_CHARS = times(40, () => '<').join('');
13+import MaybeSigil from './MaybeSigil';
14+import { sigil, reactRenderer } from 'urbit-sigil-js';
3215
33−const INVERTED_COLORWAY = ['#fff', '#000'];
16+function makeSigil(size, patp, colors) {
17+ const config = {
18+ patp: patp,
19+ colors: colors,
20+ margin: 0,
21+ renderer: reactRenderer,
22+ };
23+
24+ // Planet
25+ if (patp.length === 14) {
26+ return sigil({
27+ ...config,
28+ size: size,
29+ });
30+ }
31+ // Star
32+ if (patp.length === 7) {
33+ return sigil({
34+ ...config,
35+ width: size * 2,
36+ height: size,
37+ full: true,
38+ });
39+ }
40+ // Galaxy
41+ if (patp.length === 4) {
42+ return sigil({
43+ ...config,
44+ size: size,
45+ full: true,
46+ });
47+ }
48+}
3449
3550const buildKeyType = permissions => {
3651 if (permissions.isOwner) {
@@ -46,139 +61,256 @@ const buildKeyType = permissions => {
4661 }
4762};
4863
64+const symbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
65+
4966/**
50− * point is Maybe<number>
51− * ticket is null | boolean
52− * address is null | Maybe<string>
67+ * Address is Maybe<string>
68+ * point is Maybe<string>
69+ * animationMode is 'slide' | 'step' | 'none'
70+ *
5371 */
54−function Passport({
55− point,
56− className,
57− ticket = null,
58− address = Nothing(),
59− onClick,
60−}) {
61− const birthday = usePointBirthday(point.getOrElse(null));
62− const visualBirthday = matchBlinkyDate(birthday);
63−
64− const name = point.matchWith({
65− Nothing: () => Nothing(),
66− Just: p => Just(ob.patp(p.value)),
67− });
68− const visualName = name.matchWith({
69− Nothing: () => '...',
70− Just: p => p.value,
72+function Passport({ address, point, inverted, animationMode, keyType }) {
73+ const [cols, rows, tile] = [35, 12, 12];
74+
75+ const loading = Nothing.hasInstance(address) || Nothing.hasInstance(point);
76+ const defaultMatrix = () => {
77+ return Matrix.new(rows, cols);
78+ };
79+
80+ const patp = ob.patp(point.value);
81+
82+ const makeMatrix = addr => {
83+ // remove the 0x
84+ const onlyTheNumberPart = addr.substring(2);
85+ // make bigNum from hex
86+ const int = new BN(onlyTheNumberPart, 16);
87+ // make a bigger number
88+ const big = int.pow(new BN(10));
89+ // parse that to a string and pad
90+ const b10 = big.toString(10).padStart(32, '0');
91+ // split the big number into parts
92+ const chunkSize = Math.ceil(b10.length / 8);
93+ const chunks = chunkStr(b10, chunkSize).map(c => new BN(c));
94+ // generate an empty matrix
95+ const startMatrix = Matrix.new(rows, cols);
96+ // do a few random walks starting at diff coordinates
97+ const [, matrix] = chunks.reduce((acc, chunk) => {
98+ return walk(chunk, acc[1]);
99+ // start with the 'random' points (not actually random, they are deterministic)
100+ }, rand(big, startMatrix));
101+
102+ return matrix;
103+ };
104+
105+ const matrix = address.matchWith({
106+ Just: ({ value }) => makeMatrix(value),
107+ Nothing: defaultMatrix,
71108 });
72109
73− const showTicket = ticket !== null;
74− const isValidTicket = !!ticket;
75− const visibleTicket = isValidTicket ? VALID_TICKET : PENDING_TICKET;
76− const visibleAddress = `${address.matchWith({
77− Nothing: () => EMPTY_ADDRESS,
78− Just: p => p.value.slice(0, VISIBLE_ADDRESS_CHAR_COUNT),
79− })}${ADDRESS_BUFFER_CHARS}`;
110+ const bgColor = inverted ? 'white' : 'black';
111+
112+ const fgColor = inverted ? 'black' : 'white';
80113
81114 return (
82− <Grid onClick={onClick} gap={4} className={cn('bg-black r8 p7', className)}>
83− <Grid.Item cols={[1, 4]}>
84− <MaybeSigil patp={name} size={64} />
85− </Grid.Item>
86− <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
87− <Flex.Item className="mono white f5 pb2">{visualName}</Flex.Item>
88− <Flex.Item as={Flex} col>
89− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
90− Birth Time
91− </Flex.Item>
92− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
93− {visualBirthday}
94− </Flex.Item>
95− </Flex.Item>
96− </Grid.Item>
97− {showTicket && (
98− <Grid.Item full className="mv3" as={Flex} col>
99− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
100− Master Ticket
101− </Flex.Item>
102− <Flex.Item
103− as={Text}
104− className={cn('arial f6 f5-ns wrap', {
105− gray4: !isValidTicket,
106− green3: isValidTicket,
107− })}>
108− {visibleTicket}
109− </Flex.Item>
110− <Flex.Item as={Text} className="mono f6 f5-ns gray4 wrap">
111− {visibleAddress}
112− </Flex.Item>
113− </Grid.Item>
114− )}
115− </Grid>
115+ <div
116+ style={{
117+ fontFamily: 'Inter',
118+ backgroundColor: bgColor,
119+ border: inverted ? '2px solid #E6E6E6' : '2px solid black',
120+ borderRadius: '20px',
121+ marginBottom: '16px',
122+ minWidth: `${16 + 16 + cols * tile}px`,
123+ display: 'flex',
124+ flexDirection: 'column',
125+ }}>
126+ <div
127+ style={{
128+ height: '64px',
129+ padding: '16px',
130+ display: 'flex',
131+ }}>
132+ <div
133+ style={{
134+ width: '100%',
135+ display: 'flex',
136+ alignItems: 'center',
137+ }}>
138+ {Just.hasInstance(point) && (
139+ <MaybeSigil patp={patp.map(ob.patp).value} />
140+ )}
141+ <div
142+ style={{
143+ marginLeft: '16px',
144+ }}>
145+ <div
146+ style={{
147+ fontWeight: '600',
148+ color: fgColor,
149+ fontSize: '16px',
150+ fontFamily: 'Source Code Pro',
151+ }}>
152+ {patp}
153+ </div>
154+ {keyType !== '' ? (
155+ <div
156+ style={{
157+ marginTop: '8px',
158+ fontWeight: '500',
159+ color: fgColor,
160+ fontSize: '14px',
161+ fontFamily: 'Source Code Pro',
162+ }}>
163+ {keyType}
164+ </div>
165+ ) : (
166+ <div />
167+ )}
168+ </div>
169+ </div>
170+ </div>
171+ <div
172+ style={{
173+ display: 'flex',
174+ flexDirection: 'column',
175+ padding: '16px',
176+ paddingTop: '0px',
177+ filter: inverted ? 'invert(1)' : '',
178+ }}>
179+ {matrix.map((row, _row) => {
180+ return (
181+ <div
182+ key={`card:row:${_row}`}
183+ style={{ display: 'flex', justifyContent: 'space-between' }}>
184+ {row.map((v, _col) => {
185+ return (
186+ <Cell
187+ row={_row}
188+ col={_col}
189+ value={typeof symbols[v] === 'undefined' ? 13 : v}
190+ spriteSheet={'a'}
191+ spriteSheetHeight={12}
192+ spriteSheetWidth={168}
193+ inverted={inverted}
194+ loading={loading}
195+ animationMode={animationMode}
196+ />
197+ );
198+ })}
199+ </div>
200+ );
201+ })}
202+ </div>
203+ </div>
116204 );
117205}
118206
119−// TODO: Passport.Mini should probably accept Maybe<> for point
120−// in order to display a pretty skeleton loading view
121−// but that requires making usePermissionsForPoint accept a Maybe<point>
122−// and I don't feel like that refactor is worth it right now, and the spec
123−// doesn't call for it, hence the TODO
124−Passport.Mini = function MiniPassport({ point, className, inverted, ...rest }) {
125− useSyncOwnedPoints([point]);
207+const Cell = props => {
208+ return (
209+ <div
210+ key={`card:cell:${props.row}:${props.col}:${props.value}`}
211+ className={
212+ props.loading
213+ ? 'blink'
214+ : props.animationMode === 'none'
215+ ? ''
216+ : `${props.animationMode}-h-${props.value}`
217+ }
218+ style={{
219+ backgroundImage: `url(/spritesheet_a.png)`,
220+ backgroundPosition:
221+ props.animationMode === 'none'
222+ ? `-${props.spriteSheetHeight * props.value}px 0px`
223+ : '',
224+ backgroundSize: `${props.spriteSheetWidth}px ${props.spriteSheetHeight}px`,
225+ width: `${props.spriteSheetHeight}px`,
226+ maxWidth: `${props.spriteSheetHeight}px`,
227+ height: `${props.spriteSheetHeight}px`,
228+ maxHeight: `${props.spriteSheetHeight}px`,
229+ animationDelay: `${props.value * 50}ms`,
230+ }}
231+ />
232+ );
233+};
126234
235+/**
236+ * point is number
237+ *
238+ */
239+function MiniPassport({ point, inverted, ...rest }) {
240+ useSyncOwnedPoints([point]);
127241 const { wallet } = useWallet();
128242 const address = need.addressFromWallet(wallet);
129243
244+ const patp = ob.patp(point);
130245 const permissions = usePermissionsForPoint(address, point);
131246 const keyType = buildKeyType(permissions);
132− const name = useMemo(() => ob.patp(point), [point]);
133−
134247 return (
135− <Grid
136− gap={4}
137− className={cn(
138− 'r8 p4',
139− {
140− 'bg-black': !inverted,
141− 'bg-white b-gray4 b1': inverted,
142− },
143− className
144− )}
248+ <div
249+ style={{
250+ fontFamily: 'Inter',
251+ backgroundColor: inverted ? 'white' : 'black',
252+ border: inverted ? '2px solid #E6E6E6' : '2px solid black',
253+ borderRadius: '20px',
254+ marginBottom: '16px',
255+ width: `${230}px`,
256+ display: 'flex',
257+ flexDirection: 'column',
258+ }}
145259 {...rest}>
146− <Grid.Item cols={[1, 4]}>
147− <Sigil
148− patp={name}
149− size={64}
150− colors={inverted ? INVERTED_COLORWAY : undefined}
151− />
152− </Grid.Item>
153− <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
154− <Flex.Item
155− className={cn('mono f6 pb2', {
156− white: !inverted,
157− black: inverted,
158− })}>
159− {name}
160− </Flex.Item>
161− <Flex.Item as={Flex} col className={cn('mono f6 uppercase')}>
162− <Flex.Item
163− as={Text}
164− className={cn({
165− gray4: !inverted,
166− black: inverted,
167− })}>
168− Key Type
169− </Flex.Item>
170− <Flex.Item
171− as={Text}
172− className={cn({
173− white: !inverted,
174− black: inverted,
175− })}>
176− {keyType || <Blinky />}
177− </Flex.Item>
178− </Flex.Item>
179− </Grid.Item>
180− </Grid>
260+ <div
261+ style={{
262+ height: '44px',
263+ padding: '16px',
264+ display: 'flex',
265+ }}>
266+ <div
267+ style={{
268+ width: '100%',
269+ display: 'flex',
270+ alignItems: 'center',
271+ }}>
272+ {makeSigil(
273+ 44,
274+ patp,
275+ inverted ? ['white', 'black'] : ['black', 'white']
276+ )}
277+ <div
278+ style={{
279+ marginLeft: '16px',
280+ display: 'flex',
281+ flexDirection: 'column',
282+ justifyContent: 'space-between',
283+ }}>
284+ <div
285+ style={{
286+ fontWeight: '600',
287+ color: inverted ? 'black' : 'white',
288+ fontSize: '14px',
289+ fontFamily: 'Source Code Pro',
290+ }}>
291+ {patp}
292+ </div>
293+ {keyType !== '' ? (
294+ <div
295+ style={{
296+ marginTop: '8px',
297+ fontWeight: '500',
298+ color: inverted ? 'black' : 'white',
299+ fontSize: '12px',
300+ fontFamily: 'Source Code Pro',
301+ }}>
302+ {keyType}
303+ </div>
304+ ) : (
305+ <div />
306+ )}
307+ </div>
308+ </div>
309+ </div>
310+ </div>
181311 );
182−};
312+}
313+
314+Passport.Mini = MiniPassport;
183315
184316export default Passport;
src/lib/card.jsnew file+191
@@ -0,0 +1,191 @@
1+const initCanvas = (canvas, size, ratio) => {
2+ const { x, y } = size;
3+ // let ctx = canvas.getContext('2d')
4+
5+ const dpi200 = window.devicePixelRatio * (200 / 72);
6+
7+ canvas.width = x * dpi200;
8+ canvas.height = y * dpi200;
9+ canvas.style.width = x + 'px';
10+ canvas.style.height = y + 'px';
11+
12+ canvas.getContext('2d').scale(dpi200, dpi200);
13+
14+ return canvas;
15+};
16+
17+// takes a
18+const mapToRange = (v, l1, h1, l2, h2) =>
19+ l2 + ((h2 - l2) * (v - l1)) / (h1 - l1);
20+
21+const seq = num => Array.from(Array(num), (_, i) => i);
22+
23+const compose = (...fns) =>
24+ fns.reduce((f, g) => (...xs) => {
25+ const r = g(...xs);
26+ return Array.isArray(r) ? f(...r) : f(r);
27+ });
28+
29+const Matrix = {
30+ get: {
31+ at: (m, r, c) => m[r][c],
32+ nn: (m, r, c) => m[r - 1][c],
33+ ne: (m, r, c) => m[r - 1][c + 1],
34+ ee: (m, r, c) => m[r][c + 1],
35+ se: (m, r, c) => m[r + 1][c + 1],
36+ ss: (m, r, c) => m[r + 1][c],
37+ sw: (m, r, c) => m[r + 1][c - 1],
38+ ww: (m, r, c) => m[r][c - 1],
39+ nw: (m, r, c) => m[r - 1][c - 1],
40+ },
41+ set: {
42+ at: (m, r, c, v) => (m[r][c] = v),
43+ nn: (m, r, c, v) => (m[r - 1][c] = v),
44+ ne: (m, r, c, v) => (m[r - 1][c + 1] = v),
45+ ee: (m, r, c, v) => (m[r][c + 1] = v),
46+ se: (m, r, c, v) => (m[r + 1][c + 1] = v),
47+ ss: (m, r, c, v) => (m[r + 1][c] = v),
48+ sw: (m, r, c, v) => (m[r + 1][c - 1] = v),
49+ ww: (m, r, c, v) => (m[r][c - 1] = v),
50+ nw: (m, r, c, v) => (m[r - 1][c - 1] = v),
51+ },
52+ coord: {
53+ at: (r, c) => ({ r: r, c: c }),
54+ nn: (r, c) => ({ r: r - 1, c: c }),
55+ ne: (r, c) => ({ r: r - 1, c: c + 1 }),
56+ ee: (r, c) => ({ r: r, c: c + 1 }),
57+ se: (r, c) => ({ r: r + 1, c: c + 1 }),
58+ ss: (r, c) => ({ r: r + 1, c: c }),
59+ sw: (r, c) => ({ r: r + 1, c: c - 1 }),
60+ ww: (r, c) => ({ r: r, c: c - 1 }),
61+ nw: (r, c) => ({ r: r - 1, c: c - 1 }),
62+ },
63+ dirs: {
64+ NN: 'nn',
65+ NE: 'ne',
66+ EE: 'ee',
67+ SE: 'se',
68+ SS: 'ss',
69+ SW: 'sw',
70+ WW: 'ww',
71+ NW: 'nw',
72+ },
73+ new: (rs, cs) => seq(rs).map(_ => seq(cs).map(_ => 0)),
74+ rows: m => m.length - 1,
75+ cols: m => m[0].length - 1,
76+};
77+
78+const chunkStr = (str, size) => {
79+ const r = new RegExp(`.{1,${size}}`, 'g');
80+ return str.match(r);
81+};
82+
83+const Base3Mapping = [
84+ Matrix.dirs.NN,
85+ Matrix.dirs.NE,
86+ Matrix.dirs.EE,
87+ Matrix.dirs.SE,
88+ Matrix.dirs.SS,
89+ Matrix.dirs.SW,
90+ Matrix.dirs.WW,
91+ Matrix.dirs.NW,
92+];
93+
94+const walk = (int, matrix) => {
95+ // convert the int to a base8 string, split to array
96+ var chunks = int.toString(8).split('');
97+
98+ // convert the int to a base10 string
99+ var b10 = int.toString(10);
100+
101+ // get number of rows and cols
102+ var rows = Matrix.rows(matrix);
103+ var cols = Matrix.cols(matrix);
104+
105+ // split the length-4 number into 2 parts, which is a number from 0-99
106+ // if the substring is empty, substitute 10
107+ var sx = parseInt(b10.substring(0, 2) || 0, 10);
108+ var sy = parseInt(b10.substring(2, 4) || 0, 10);
109+
110+ // generate a starting coordinate
111+ var coord = {
112+ c: ~~mapToRange(sx, 0, 99, 0, cols),
113+ r: ~~mapToRange(sy, 0, 99, 0, rows),
114+ };
115+
116+ var vect;
117+ var next;
118+
119+ for (var i = 0; i < chunks.length; i++) {
120+ // Each chunk is a number from 0-7 which refers to a direction assigned in Base3Mapping.
121+ // Get the current chunk, and then get the direction it has been paired with.
122+ vect = Base3Mapping[chunks[i]];
123+
124+ // generate the next coordinate to go to, based on the current coordinate and the direction stored in vect.
125+ next = Matrix.coord[vect](coord.r, coord.c);
126+
127+ // If at an edge of the matrix, substitute the opposite direction.
128+ if (next.r < 0) vect = Matrix.dirs.SS;
129+ if (next.c < 0) vect = Matrix.dirs.EE;
130+ if (next.r > rows) vect = Matrix.dirs.NN;
131+ if (next.c > cols) vect = Matrix.dirs.WW;
132+
133+ // If at in the corner of the matrix, substitute an alternative direction.
134+ if (next.r === 0 && coord.c === 0) vect = Matrix.dirs.SE;
135+ if (next.r > rows && coord.c === 0) vect = Matrix.dirs.NE;
136+ if (next.r === 0 && coord.c > cols) vect = Matrix.dirs.SW;
137+ if (next.r > rows && coord.c > cols) vect = Matrix.dirs.NW;
138+
139+ // get the next coordinate again in case there was an edge/cornercase
140+ next = Matrix.coord[vect](coord.r, coord.c);
141+
142+ // increment the value at the next coordinate.
143+ Matrix.set.at(
144+ matrix,
145+ next.r,
146+ next.c,
147+ Matrix.get.at(matrix, next.r, next.c) + 1
148+ );
149+
150+ // set the new current coordinate.
151+ coord = next;
152+ }
153+
154+ // return a 2-ary array in order to pass both values to the next function in the compose pipeline.
155+ return [int, matrix];
156+};
157+
158+const rand = (int, matrix) => {
159+ // break base10 integer into chunks with length of 4
160+ var chunks = chunkStr(int.toString(10), 4);
161+ var cols = Matrix.cols(matrix);
162+ var rows = Matrix.rows(matrix);
163+ var ch;
164+ var x;
165+ var y;
166+ var c;
167+ var r;
168+
169+ for (var i = 0; i < chunks.length; i++) {
170+ // for each chunk in chunks
171+ ch = chunks[i];
172+
173+ // split the length-4 number into 2 parts, which is a number from 0-99
174+ // if the substring is empty, substitute 10
175+ x = parseInt(ch.substring(0, 2) || 0, 10);
176+ y = parseInt(ch.substring(2, 4) || 0, 10);
177+
178+ // map num from 0-99 to 0 - num of cols, then use ~~ for fast math.floor
179+ // c (col) and r (row) are coordinates
180+ c = ~~mapToRange(x, 0, 99, 0, cols);
181+ r = ~~mapToRange(y, 0, 99, 0, rows);
182+
183+ // increment number at the coordinate
184+ Matrix.set.at(matrix, r, c, Matrix.get.at(matrix, r, c) + 1);
185+ }
186+
187+ // return a 2-ary array in order to pass both values to the next function in the compose pipeline.
188+ return [int, matrix];
189+};
190+
191+export { chunkStr, initCanvas, seq, Matrix, compose, mapToRange, walk, rand };
src/views/Point.js+9−4
@@ -4,13 +4,13 @@ import { Grid } from 'indigo-react';
44import { azimuth } from 'azimuth-js';
55
66import { usePointCursor } from 'store/pointCursor';
7+import { useWallet } from 'store/wallet';
78
89import View from 'components/View';
910import Greeting from 'components/Greeting';
1011import Passport from 'components/Passport';
1112import { ForwardButton, BootArvoButton } from 'components/Buttons';
1213import { matchBlinky } from 'components/Blinky';
13−import SigilDownloader from 'components/SigilDownloader';
1414
1515import * as need from 'lib/need';
1616import useInvites from 'lib/useInvites';
@@ -22,6 +22,7 @@ export default function Point() {
2222 const { pop, push, names } = useLocalRouter();
2323 const { pointCursor } = usePointCursor();
2424
25+ const { wallet } = useWallet();
2526 const point = need.point(pointCursor);
2627
2728 const {
@@ -87,12 +88,16 @@ export default function Point() {
8788 // sync the current cursor
8889 useSyncOwnedPoints([point]);
8990
91+ const address = need.addressFromWallet(wallet);
92+
9093 return (
9194 <View pop={pop} inset>
9295 <Greeting point={point} />
93− <SigilDownloader point={point}>
94− <Passport point={Just(point)} />
95− </SigilDownloader>
96+ <Passport
97+ point={Just(point)}
98+ address={Just(address)}
99+ animationMode={'none'}
100+ />
96101 <Grid className="pt2">
97102 {inviteButton}
98103 <Grid.Item