import React, { Component } from 'react'; import { Token } from '../core/token'; import { warning } from '../utils/helpers'; import { ValidationMap } from 'prop-types'; import { Dictionary } from 'infinitymint/dist/app/helpers'; import { KeyValue } from 'infinitymint/dist/app/interfaces'; export interface RenderClass extends Component { onWindowResize(shouldReload?: boolean): void; renderToken(): string; load(): Promise; getDataURL(): string; getContainerId(): string; } export interface RenderProps extends Dictionary { scaleFactor?: number; } export interface RenderComponentProps extends KeyValue { children?: React.ReactNode; token: Token; rendererProps?: RenderProps; } export interface RenderComponentState extends KeyValue { loaded?: boolean; seed?: string; rendererProps?: RenderProps; savedRender?: any; } /** * Extend this class to create a new renderer */ export abstract class RenderComponent extends Component implements RenderClass { propTypes?: React.WeakValidationMap; contextType?: React.Context; contextTypes?: ValidationMap; childContextTypes?: ValidationMap; defaultProps?: Partial; displayName?: string = 'RenderComponent'; getDerivedStateFromProps?: React.GetDerivedStateFromProps; getDerivedStateFromError?: React.GetDerivedStateFromError; onWindowResize(shouldReload?: boolean): void { throw new Error('Method not implemented.'); } renderToken(): string { throw new Error('Method not implemented.'); } load(): Promise { throw new Error('Method not implemented.'); } getDataURL(): string { throw new Error('Method not implemented.'); } getContainerId(): string { throw new Error('Method not implemented.'); } /** * base64encoded image/png data url of the rendered image, created after load is called */ protected renderedImage: string; } /** * Default Render class using the canvas element */ export default class DefaultRenderer extends RenderComponent { constructor(props: { children?: React.ReactNode; token: Token; rendererProps?: RenderProps; }) { super(props); this.state = { loaded: false, seed: btoa('IM_' + Math.floor(Math.random() * 1000)), rendererProps: props.rendererProps, }; } /** * * @param shouldReload * @returns */ onWindowResize(shouldReload = true) { let canvas = this.getCanvasElement(); if (!canvas) return; let clientHeight = (Math.ceil(canvas.parentElement.clientWidth / 16) * 16) / 4; if (this.state.rendererProps?.scaleFactor) clientHeight *= this.state.rendererProps.scaleFactor; canvas.width = clientHeight; // equals window dimension canvas.height = clientHeight; canvas.style.height = clientHeight + 'px'; canvas.style.width = clientHeight + 'px'; if (this.renderedImage && shouldReload) { let image = new Image(); image.src = this.renderedImage; image.onload = () => { this.drawScaledImage(image, canvas.getContext('2d')); }; } else if (shouldReload) { //reload it this.load(); } return { width: clientHeight, height: clientHeight, }; } componentDidMount() { let element = document.getElementById(this.getContainerId()); if (!element) { warning('no element found: ' + this.getContainerId()); return; } element.appendChild(this.createCanvasElement()); //scale the canvas pre this.onWindowResize(false); //then do resize listeners const resize = () => { this.onWindowResize(); }; window.removeEventListener('resize', resize); window.addEventListener('resize', resize); //overwrite component will unmount this.componentWillUnmount = () => { window.removeEventListener('resize', resize); this.getCanvasElement().remove(); }; (async () => { await this.load(); this.setState( { loaded: true, }, () => { //finally render the canvas this.getCanvasElement().style.display = 'block'; } ); })(); } async load() { let canvas = this.getCanvasElement(); let context = canvas.getContext('2d'); let path = await this.props.token.getBase64EncodedPath('path'); let image = await this.loadImage(path); this.drawScaledImage(image, context); let assets = this.props.token.getAssets() || []; for (let i = 0; i < assets.length; i++) { let asset = assets[i]; if (!asset) continue; try { let assetPath = await this.props.token.getBase64EncodedAsset( asset.assetId ); let assetImage = await this.loadImage(assetPath); this.drawScaledImage(assetImage, context); } catch (error) { warning(error); } } return this.renderToken(); } getDataURL() { if (!this.renderedImage) this.renderToken(); return this.renderedImage; } renderToken() { let canvas = this.getCanvasElement(); if (!canvas) return; let canvasCopy = document.createElement('canvas'); canvasCopy.width = canvas.width; canvasCopy.height = canvas.height; canvasCopy.getContext('2d').drawImage(canvas, 0, 0); // copy the 'old' canva this.renderedImage = canvasCopy.toDataURL('image/png', 1.0); return this.renderedImage; } async loadImage(src: string) { let image = new Image(); image.src = src; await new Promise((resolve) => { image.onload = resolve; }); return image; } drawScaledImage( img: HTMLImageElement, context: CanvasRenderingContext2D, settings?: { x?: number; y?: number; } ) { let canvas = context.canvas; // get the scale // it is the min of the 2 ratios let scaleFactor = Math.max( canvas.width / img.width, canvas.height / img.height ); // Finding the new width and height based on the scale factor let newWidth = img.width * scaleFactor; let newHeight = img.height * scaleFactor; // get the top left position of the image // in order to center the image within the canvas let x = settings?.x || canvas.width / 2 - newWidth / 2; let y = settings?.y || canvas.width / 2 - newHeight / 2; // When drawing the image, we have to scale down the image // width and height in order to fit within the canvas context.drawImage(img, x, y, newWidth, newHeight); } componentDidUpdate( prevProps: Readonly, prevState: Readonly, snapshot?: any ): void { if (!this.state.loaded) return; let shouldQuit = prevProps.token.pathId === this.props.token.pathId; //check if assets have changed let oldAssets = prevProps.token.getAssets(); let newAssets = this.props.token.getAssets(); newAssets.forEach((asset, index) => { if (!asset && !oldAssets[index]) return; if (!asset && oldAssets[index]) shouldQuit = false; if (!oldAssets.find((val) => val.assetId === asset.assetId)) shouldQuit = false; }); if (shouldQuit) return; this.setState({ loaded: false, }); let canvas = this.getCanvasElement(); canvas.style.display = 'none'; let context = canvas.getContext('2d'); context.clearRect(0, 0, canvas.height, canvas.width); //reload this.load().then(() => { canvas.style.display = 'block'; this.setState({ loaded: true, }); }); } getContainerId() { return ( this.props.token.tokenId + '_' + this.props.token.project.name + '_' + this.props.token.project?.deployedProject?.network?.name + '_' + this.state.seed ); } getCanvasID() { return this.getContainerId() + '_canvas'; } getCanvasElement() { return document.getElementById(this.getCanvasID()) as HTMLCanvasElement; } createCanvasElement() { let canvas = document.createElement('canvas'); canvas.style.display = 'none'; canvas.style.marginLeft = 'auto'; canvas.style.marginRight = 'auto'; canvas.setAttribute('id', this.getContainerId() + '_canvas'); return canvas; } render() { return (
{!this.state.loaded ? (

Rendering

InfinityMint

{this.props.token.getPathName()}{' '} tokenId {this.props.token.tokenId}

{this.props.token.getName() || 'Unknown Token'}

) : ( <> )}
); } }