The Engineering Secrets Behind Our WOLCANO Project.

Wolcano’s ONE Canvas Challenge is inspired by r/Place, a collaborative project and social experiment hosted on the social networking site Reddit that began on April Fools‘ Day in 2017. The experiment involved an online canvas located at a subreddit called r/place, which registered users could edit by changing the color of a single pixel from a 16-color palette. After each pixel was placed, a timer prevented the user from placing any pixels for a period varying from 5 to 20 minutes.

A lot of interesting things happened during the event. However, as our engineering team try to upgrade and reconstruct this idea since we need to catch more data and mined it as an NFT once it finished. There are a lot potential risk and new challenges, so how actually we can solve multi-person collaborative pixel painting structure?

Before designing this feature point, some technical points need to be overcome:

  • The system should support at least 100,000 online users
  • The drawing board is not less than 1000 x 2000 pixels
  • All clients must be synchronized with the same view of the current board status to ensure that users with different boards can collaborate
  • To ensure a sense of collective participation of users, the frequency of individual placement of pixels must be restricted
  • The system allocates resources according to the number of participants in the event, that is, it can be dynamically configured to prevent accidents in the event

Backend Development

To quickly respond to the verification of ideas and overcome technical points, the back-end borrows AWS API Gateway, Lambda, and Dynamodb modules to quickly achieve development needs

Its simple structure is as follows

With the help of Amazon API Gateway (WebSocket API), a continuous link API can be built. When the user draws, the API Gateway transmits data to DynamoDB through the Lambda proxy. DynamoDB can create a “Trigger” to transmit data events to Lambda, and Lambda responds to data changes. By reading the user connect ID saved by DynamoDB, the modification is synchronized to all online users, and the synchronization function of all clients and the artboard state is realized.

What needs to be added here is the event-driven function of DynamoDB Streams. DynamoDB Stream is like the changelog of a DynamoDB table-every time an item is created, updated, or deleted, a record is written to the DynamoDB stream. This stream has two interesting functions. First, it is sorted by time, so older records appear before newer records. Second, it is persistent because it retains the changes made to the DynamoDB table in the past 24 hours. Compared to temporary publish-subscribe mechanisms such as SNS, this is a useful attribute because you can reprocess the most recent records.

When setting up DynamoDB streams, you need to set the stream view type. This specifies which data about the changed item will be included in each record in the stream. The stream view types are KEYS_ONLY, NEW_AND_OLD_IMAGES, NEW_AND_OLD_IMAGES, NEW_IMAGE, OLD_IMAGE. The real power of DynamoDB Streams is that you integrate them with Lambda. You can set up Streams to trigger a Lambda function, and then the function can operate on the records in the Stream.

The core logic of Lambda’s response to DynamoDB’s data flow is similar to the following example

import json
import boto3
import botocore

def lambda_handler(event, context):
    place_connection_table = "XXXXConnection"
    place_pixel_table = "XXXXX"
    dynamodb = boto3.resource("dynamodb")
    connectionTable = dynamodb.Table(place_connection_table)
    resp = connectionTable.scan();
    api_client = boto3.client('apigatewaymanagementapi', endpoint_url = f"")
        for record in event['Records']:
            if record['eventName'] == 'INSERT':
                handle_insert(record, api_client, resp["Items"])
            elif record['eventName'] == 'MODIFY':
                handle_modify(record, api_client, resp["Items"])
            elif record['eventName'] == 'REMOVE':
        return {'statusCode': 200}
    except Exception as e:
        return {'statusCode': 500}

def handle_insert(record, api_client, items):
    print("Handling INSERT Event")

    newImage = record['dynamodb']['NewImage']
    data = [{"color": newImage["color"]["S"], "location": newImage["location"]["S"]}]
    dataJson = json.dumps(data)
    for item in items:
        print("item", item)
            api_client.post_to_connection(Data = dataJson, ConnectionId = item["connectionId"])
        except Exception as e:
            print("excetion-insert", e)

def handle_modify(record, api_client, items):
    print("Handling MODIFY Event")
    newImage = record['dynamodb']['NewImage']
    data = [{"color": newImage["color"]["S"], "location": newImage["location"]["S"]}]
    dataJson = json.dumps(data)
    for item in items:
        print("item", item)
            api_client.post_to_connection(Data = dataJson, ConnectionId = item["connectionId"])
        except Exception as e:
            print("exception-modify", e)
def handle_remove(record):
    print("Handling REMOVE Event")

Worth mentioning that API Gateway has three Route Keys by default. $connect needs to save the user Connect Id in DynamoDB in the business logic, which is used to synchronize other people’s drawing data to all online users. The code example is as follows:

import boto3

def lambda_handler(event, context):
    connection_id = event["requestContext"]["connectionId"]
    connection_time = event["requestContext"]["connectedAt"]
    place_connection_table = event['stageVariables']['place_connection']
    place_pixel_table = event['stageVariables']['place_pixel']
    dynamodb = boto3.resource("dynamodb")
    connectionTable = dynamodb.Table(place_connection_table)
    record = {"connectionId": connection_id, "createTime": connection_time}
    connectionTable.put_item(Item = record)

And $disconnect will be called when the user leaves. To ensure the accuracy of online user data, it must be removed from DynamoDB here. The code example is as follows:

import json
import boto3

def lambda_handler(event, context):
    placeConnectionTable = event['stageVariables']['place_connection']
    connectionTable = boto3.resource("dynamodb").Table(placeConnectionTable)
    connectionTable.delete_item(Key = {"connectionId": connectionId})

    return {'statusCode': 200 }

In addition, if you want, you can also choose Firebase service. Its authentication and real-time database module can fully meet most of the back-end needs, even you hardly need to code, except that its services are unavailable in some areas and service fees are used.

Frontend Development

The front-end project of building multi-person collaborative pixel painting also overcomes some technical difficulties, including but not limited to mainstream platforms (desktop WEB, mobile WEB), user-friendly painting experience, and real-time update of the canvas status.

The front end uses tailwindcss to quickly build multi-terminal compatible pages, PixiJS to create animations or manage interactive images, TweenMax to build tweens, and WebSocket to respond to server-side data and update to the canvas.

First, through PixiJS, you need to create a canvas and draw a grid and register to listen for events. The core code is as follows:

 		var application = new PIXI.Application(window.innerWidth, window.innerHeight - 60, { antialias: false, backgroundColor: 0xcccccc });
    container = new PIXI.Container();
    graphics = new PIXI.Graphics();
    graphics.beginFill(0xffffff, 1);
    graphics.drawRect(0, 0, gridSize[0] * squareSize[0], gridSize[1] * squareSize[1]);
    graphics.interactive = true;
    graphics.on('pointerdown', onDown);
    graphics.on('pointermove', onMove);
    graphics.on('pointerup', onUp);
    graphics.on('pointerupoutside', onUp);
    graphics.position.x = -graphics.width / 2;
    graphics.position.y = -graphics.height / 2;
    gridLines = new PIXI.Graphics();
    gridLines.lineStyle(0.5, 0xeeeeee, 1);
    gridLines.alpha = 0;
    gridLines.position.x = graphics.position.x;
    gridLines.position.y = graphics.position.y;
    for (let i = 0; i <= gridSize[0]; i++) {
        drawLine(0, i * squareSize[0], gridSize[0] * squareSize[0], i * squareSize[0]);
    for (let j = 0; j <= gridSize[0]; j++) {
        drawLine(j * squareSize[1], 0, j * squareSize[1], gridSize[1] * squareSize[1]);
    window.onresize = onResize;

In order for users to better interact with the canvas, fine adjustments have been made in moving, zooming in and out, and using TweenMax to achieve operations that are more in line with daily interactions, a simple example is, 0.1, { x: scale, y: scale, ease: Power3.easeOut });

WOLCANO encourages multi-person collaborative creation. For this reason, it will be judged whether it has passed the cool-down period before each painting, and the cool-down time will be updated for each operation.

getTimestamp().then(timestamp => {
            const data = {
                uid: uid,
                timestamp: timestamp,
                color: color,
                location: x + 'x' + y,
                token: token
            if(coolCount == 0){
                const params = { action: "drawRoute", "data": data};
                renderPixelData(x + 'x' + y, color);
        }).catch(error => {
            console.warn("error", error);

https://mindcoord.cnHope it is helpful to understand the logic and structure behind our fun WOLCANO ONE Canvas Challenge, and MindCoord Engineering team will post more blogs on some interesting topics. Follow our Twitter if you want to know more!

And, yes! Go wolcano and have some fun!