Browse Source

Society generator (#3141)

* Society generator

* Modal popup

* Log accountId & hex

* Spacing

* Reduce for addressToBits
Jaco Greeff 4 years ago
parent
commit
f1277c3ede

+ 52 - 2
packages/page-society/src/Overview/Member.tsx

@@ -4,9 +4,11 @@
 
 import { DeriveSocietyMember } from '@polkadot/api-derive/types';
 
-import React from 'react';
-import { AddressSmall, Tag } from '@polkadot/react-components';
+import React, { useEffect, useRef, useState } from 'react';
+import { AddressSmall, Icon, Modal, Tag } from '@polkadot/react-components';
+import { useApi, useToggle } from '@polkadot/react-hooks';
 
+import drawCanary from '../draw/canary';
 import { useTranslation } from '../translate';
 
 interface Props {
@@ -15,8 +17,27 @@ interface Props {
   value: DeriveSocietyMember;
 }
 
+const CANVAS_STYLE = {
+  display: 'block',
+  margin: '0 auto'
+};
+
 function Member ({ className = '', isHead, value: { accountId, strikes } }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
+  const { api } = useApi();
+  const canvasRef = useRef<HTMLCanvasElement | null>(null);
+  const [canInk] = useState(api.genesisHash.eq('0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe'));
+  const [isInkShowing, toggleInk] = useToggle();
+
+  useEffect((): void => {
+    if (canvasRef.current) {
+      const ctx = canvasRef.current.getContext('2d');
+
+      if (ctx) {
+        drawCanary(ctx, accountId);
+      }
+    }
+  });
 
   return (
     <tr className={className}>
@@ -36,6 +57,35 @@ function Member ({ className = '', isHead, value: { accountId, strikes } }: Prop
       <td className='number top'>
         {strikes.toString()}
       </td>
+      <td>
+        {canInk && (
+          <>
+            <Icon
+              icon='pen-nib'
+              onClick={toggleInk}
+            />
+            {isInkShowing && (
+              <Modal
+                header={t('design samples')}
+                size='large'
+              >
+                <Modal.Content>
+                  <canvas
+                    height={525}
+                    ref={canvasRef}
+                    style={CANVAS_STYLE}
+                    width={800}
+                  />
+                </Modal.Content>
+                <Modal.Actions
+                  cancelLabel={t<string>('Close')}
+                  onCancel={toggleInk}
+                />
+              </Modal>
+            )}
+          </>
+        )}
+      </td>
     </tr>
   );
 }

+ 2 - 1
packages/page-society/src/Overview/Members.tsx

@@ -30,7 +30,8 @@ function Members ({ className = '', info }: Props): React.ReactElement<Props> {
 
   const header = useMemo(() => [
     [t('members'), 'start', 3],
-    [t('strikes')]
+    [t('strikes')],
+    [undefined, 'mini']
   ], [t]);
 
   return (

+ 217 - 0
packages/page-society/src/draw/canary.ts

@@ -0,0 +1,217 @@
+// Copyright 2017-2020 @polkadot/app-society authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+// Adapted (wih permission) from https://www.w3schools.com/code/tryit.asp?filename=GGIGKE2GG7N1
+
+import { AccountId } from '@polkadot/types/interfaces';
+
+// const DEFAULT_FN = (ctx: CanvasRenderingContext2D, path: Path2D) => ctx.fill(path);
+const PADD = 25;
+const SIZE = 250;
+
+function canary (ctx: CanvasRenderingContext2D, w: number, h: number, s: number, f: (ctx: CanvasRenderingContext2D, path: Path2D) => void): void {
+  const path = new Path2D('M373.1,126.9c-5.2-4.1-11.4-9.7-22.7-11.1c-10.6-1.4-21.4,5.7-28.7,10.4c-7.3,4.7-21.1,18.5-26.8,22.7 c-5.7,4.2-20.3,8.1-43.8,22.2s-115.7,73.3-115.7,73.3l24,0.3L52.4,299.8h10.7l-15.4,11.7c0,0,13.6,3.6,25-3.6l0,3.3 c0,0,127.4-50.2,152-37.2l-15,4.4c1.3,0,25.5,1.6,25.5,1.6s0.8,15.1,15.4,24.8c14.6,9.6,14.9,14.9,14.9,14.9s-7.6,3.1-7.6,7 c0,0,11.2-3.4,21.6-3.1c10.4,0.3,19.5,3.1,19.5,3.1s-0.8-4.2-10.9-7c-10.2-2.9-20.1-13.8-25-19.8c-4.9-6-8.3-16.7-4.1-27.4 c3.5-9.1,15.7-14.1,40.9-27.1c29.7-15.4,36.5-26.8,40.7-35.7c4.2-8.9,10.4-26.6,13.9-34.9c4.4-10.7,9.8-16.4,14.3-19.8 c4.4-3.4,24.5-10.9,24.5-10.9S378,130.8,373.1,126.9z');
+
+  ctx.save();
+  ctx.translate(w / 2, h / 2);
+  ctx.scale(s / 440, s / 440);
+  ctx.translate(-220, -220);
+  f(ctx, path);
+  ctx.restore();
+}
+
+function addressToBits (publicKey: Uint8Array): boolean[] {
+  return publicKey.reduce((bits: boolean[], byte): boolean[] => {
+    for (let j = 0; j < 8; ++j) {
+      bits.push((byte & (1 << (7 - j))) !== 0);
+    }
+
+    return bits;
+  }, []);
+}
+
+function ring (ctx: CanvasRenderingContext2D, r: number, bits: boolean[], f: (ctx: CanvasRenderingContext2D, on: boolean) => void): void {
+  ctx.save();
+  ctx.translate(0.5, 0.5);
+
+  for (let i = 0; i < bits.length; i++) {
+    ctx.save();
+    ctx.rotate(Math.PI * 2 / bits.length * i);
+    ctx.translate(0, -r);
+    f(ctx, bits[i]);
+    ctx.restore();
+  }
+
+  ctx.restore();
+}
+
+function splitRows (bits: boolean[], rows: number[]) {
+  let i = 0;
+
+  // eslint-disable-next-line no-return-assign
+  return rows.map((r) => bits.slice(i, i += r));
+}
+
+function tattoo (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
+  const rows = splitRows(bits, [71, 61, 51, 41, 32]);
+
+  for (let i = 0; i < rows.length; ++i) {
+    ring(ctx, 0.5 - (31 / 500) * (i + 0.5), rows[i], (ctx, on) => {
+      if (on) {
+        ctx.beginPath();
+        ctx.arc(0, 0, 8 / 500, 0, 2 * Math.PI);
+        ctx.fillStyle = 'black';
+        ctx.fill();
+      }
+    });
+  }
+
+  ctx.lineWidth = 10;
+  canary(ctx, 1, 1, 200 / 500, (ctx, path) => ctx.stroke(path));
+}
+
+function tattooSpiro (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
+  const cycles = 8;
+  const limit = 0.75;
+  const dot = 8 / 500;
+
+  ctx.save();
+  ctx.translate(0.5 - dot, 0.5 + dot);
+  ctx.fillStyle = 'black';
+
+  let radius = 0.5 - dot;
+
+  for (let i = 0; i < bits.length; i++) {
+    radius -= 0.5 / bits.length * limit / (radius * 4);
+    ctx.rotate(Math.PI * 2 / bits.length * cycles / (radius * 4));
+    ctx.save();
+    ctx.translate(0, -radius);
+    ctx.beginPath();
+    ctx.arc(0, 0, (bits[i] ? dot : dot / 2), 0, 2 * Math.PI);
+    ctx.fillStyle = bits[i] ? 'black' : '#e6007a';
+    ctx.fill();
+    ctx.restore();
+  }
+
+  ctx.restore();
+  ctx.lineWidth = 10;
+  canary(ctx, 1 - dot, 1 + dot, 220 / 500, (ctx, path) => ctx.stroke(path));
+}
+
+function tattooPink (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
+  const rows = splitRows(bits, [71, 61, 51, 41, 32]);
+
+  for (let i = 0; i < rows.length; ++i) {
+    ring(ctx, 0.5 - (31 / 500) * (i + 0.5), rows[i], (ctx, on) => {
+      ctx.beginPath();
+      ctx.arc(0, 0, (on ? 8 : 4) / 500, 0, 2 * Math.PI);
+      ctx.fillStyle = on ? 'black' : '#e6007a';
+      ctx.fill();
+    });
+  }
+
+  canary(ctx, 1, 1, 220 / 500, (ctx, path) => ctx.fill(path));
+}
+
+function tattoo2 (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
+  const rows = splitRows(bits, [64, 64, 64, 64]);
+
+  for (let i = 0; i < rows.length; ++i) {
+    ring(ctx, 0.5 - (36 / 500) * (i + 0.5), rows[i], (ctx, on) => {
+      if (on) {
+        ctx.beginPath();
+        ctx.moveTo(0, -18 / 500);
+        ctx.lineTo(0, 18 / 500);
+        ctx.lineWidth = 0.01;
+        ctx.stroke();
+      }
+    });
+  }
+
+  canary(ctx, 1, 1, 220 / 500, (ctx, path) => ctx.fill(path));
+}
+
+function tattoo2b (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
+  const rows = splitRows(bits, [128, 128]);
+
+  for (let i = 0; i < rows.length; ++i) {
+    ring(ctx, 0.5 - (36 / 500) * (i + 0.5), rows[i], (ctx, on) => {
+      if (on) {
+        ctx.beginPath();
+        ctx.moveTo(0, -18 / 500);
+        ctx.lineTo(0, 18 / 500);
+        ctx.lineWidth = 0.01;
+        ctx.stroke();
+      }
+    });
+  }
+
+  ctx.lineWidth = 6;
+  canary(ctx, 1, 1, 350 / 500, (ctx, path) => ctx.stroke(path));
+}
+
+function tattoo3 (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
+  ctx.lineWidth = 0.01;
+
+  for (let i = 0; i < 8; ++i) {
+    for (let j = 0; j < 32; ++j) {
+      if (bits[i * 32 + j]) {
+        ctx.save();
+        ctx.translate((j + 0.5) / 32, i / 8);
+        ctx.beginPath();
+        ctx.moveTo(0, 0);
+        ctx.lineTo(0, 1 / 8);
+        ctx.stroke();
+        ctx.restore();
+      }
+    }
+  }
+
+  ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+  canary(ctx, 1, 1, 1, (ctx, path) => ctx.fill(path));
+  ctx.lineWidth = 6;
+  canary(ctx, 1, 1, 1, (ctx, path) => ctx.stroke(path));
+}
+
+export default function draw (ctx: CanvasRenderingContext2D, accountId: AccountId): void {
+  console.log(`Generating ink for ${accountId.toString()} as ${accountId.toHex()}`);
+
+  const bits = addressToBits(accountId.toU8a());
+
+  ctx.save();
+  ctx.translate(0, 0);
+  ctx.scale(SIZE, SIZE);
+  tattoo(ctx, bits);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(SIZE + PADD, 0);
+  ctx.scale(SIZE, SIZE);
+  tattooPink(ctx, bits);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(0, SIZE + PADD);
+  ctx.scale(SIZE, SIZE);
+  tattoo2(ctx, bits);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(SIZE + PADD, SIZE + PADD);
+  ctx.scale(SIZE, SIZE);
+  tattoo3(ctx, bits);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate((SIZE + PADD) * 2, 0);
+  ctx.scale(SIZE, SIZE);
+  tattooSpiro(ctx, bits);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate((SIZE + PADD) * 2, SIZE + PADD);
+  ctx.scale(SIZE, SIZE);
+  tattoo2b(ctx, bits);
+  ctx.restore();
+}

+ 1 - 1
packages/react-components/src/Modal/types.tsx

@@ -20,7 +20,7 @@ export interface ModalProps {
 export interface ActionsProps {
   className?: string;
   cancelLabel?: string;
-  children: React.ReactNode;
+  children?: React.ReactNode;
   withOr?: boolean;
   onCancel: () => void;
 }