In previous episode, we prepared an architecture of the project and environment for development. Today, we are going to write a client-side application for handling canvas and captcha in the browser.
PIXI.js
To control canvas we are going to use PIXI.js, so move to the project directory and install by running:
yarn add pixi.js
Then import in the main component of the canvas.
import * as PIXI from 'pixi.js';
To use the PIXI library, we need to create a PIXI Application and append view somewhere on the website. Because we are working on the widget-like tool, application view is going to be attached inside of the component. The application we will create on the first mounting with componentDidMount
method or even in the constructor. In my case, the second option is cleaner, because I won't be switching between different components.
export class App extends React.Component<any, IApp> {
constructor(props : any) {
super(props);
this.state = {
app: new PIXI.Application({
width: 480,
height: 280,
backgroundColor: 0xeeeeee,
resolution: window.devicePixelRatio || 1,
}),
};
}
// ...
}
On the first line, you see that I'm telling that interface IApp is going to define how the state of the component is going to looks like. Now, just PIXI application under "app" key is fine.
interface IApp {
app: PIXI.Application
}
In the initial state, I created new PIXI Application instance with the width and height of the canvas and very bright colour in the background.
View for our application we can append in the previously mentioned componentDidMount like below:
componentDidMount() {
document.getElementById('devcaptcha-container').appendChild(this.state.app.view);
}
And inside render method we need to create HTML elemenet with devcaptcha-container id:
render() {
return <div id={"devcaptcha-container"}/>;
}
If you did everything well, you should be able to render rectangle somewhere in your application.
Canvas Elements
Now, we need to add canvas elements for captcha. My captcha will contain:
- instruction how to use captcha,
- white stripes on the top and the bottom as the background for text and button,
- button for submitting a captcha response,
- image background with a picture from the backend with a drawn puzzle,
- puzzle element to drag and drop to match with this from backend,
PIXI has various classes for representing canvas elements. For the background, we can use Sprite and alternative construction method, which as argument accept image URL.
const background = PIXI.Sprite.from('https://placeholderimg.jpg');
And then set some properties. In this case, we want to stretch the background on the entire canvas. Initial anchor point (position point) of the elements in PIXI is in the top-left corner. Co our background sprite should start at position 0,0 (top-left edge of the canvas) and be 100% width and height. We can use for that previously saved reference to the object of PIXI application, and view.
background.width = this.state.app.view.width;
background.height = this.state.app.view.height;
And finally, we can append this background object inside view:
this.state.app.stage.addChild(background);
Awesome! At this point, you should see your image in the background. Now let add white, background stripes. We are going to use for this Graphics class, which is responsible for primitive, vector shapes. With this class, we can add two 32px stripes for top and the bottom and two 4px thin shadow lines.
// top stripe
const stripes = new PIXI.Graphics();
stripes.beginFill(0xffffff);
stripes.drawRect(0, 0,
this.state.app.view.width,
32
);
stripes.endFill();
// bottom stripe
stripes.beginFill(0xffffff);
stripes.drawRect(0,
this.state.app.view.height - 32,
this.state.app.view.width,
32
);
// top shadow
stripes.beginFill(0xdddddd, 0.5);
stripes.drawRect(0, 32,
this.state.app.view.width,
4
);
stripes.endFill();
// bottom shadow
stripes.beginFill(0xdddddd, 0.5);
stripes.drawRect(0,
this.state.app.view.height - 36,
this.state.app.view.width,
4
);
stripes.endFill();
this.state.app.stage.addChild(stripes);
We also need a button for submitting the captcha response. We will use the same class as previously. But this time, we will set properties for interactive and event listener.
// submit button
const submitButton = new PIXI.Graphics();
submitButton.interactive = true;
submitButton.buttonMode = true;
submitButton.on('pointerdown', () => {
// on mouse fire
});
submitButton.beginFill(0x222222);
submitButton.drawRect(this.state.app.view.width - 112,
this.state.app.view.height - 64,
96,
48
);
submitButton.endFill();
this.state.app.stage.addChild(submitButton);
Text on the top will inform how to solve captcha:
// instruction
const basicText = new PIXI.Text('Move the jigsaw to the correct position to solve captcha.', {
fontFamily: 'Arial',
fontSize: 16,
fill: '#000000',
});
basicText.x = 8;
basicText.y = 8;
this.state.app.stage.addChild(basicText);
And the second on the button:
// text on the submit button
const submitButtonText = new PIXI.Text('Submit', {
fontFamily: 'Arial',
fontSize: 14,
fill: '#ffffff',
});
submitButtonText.x = this.state.app.view.width - 112 + 40;
submitButtonText.y = this.state.app.view.height - 64 + 16;
this.state.app.stage.addChild(submitButtonText);
To make this button look better, I added icon:
// icon on the submit button
const submitButtonIcon = PIXI.Sprite.from('https://i.imgur.com/mgWUPWc.png');
submitButtonIcon.width = 24;
submitButtonIcon.height = 24;
submitButtonIcon.x = this.state.app.view.width - 112 + 12;
submitButtonIcon.y = this.state.app.view.height - 64 + 12;
this.state.app.stage.addChild(submitButtonIcon);
And finally, two more labels for Terms of Service and Privacy Policy:
// privacy policy
const textPrivacy = new PIXI.Text('Privacy', {
fontFamily: 'Arial',
fontSize: 12,
fill: '#777777',
});
textPrivacy.interactive = true;
textPrivacy.buttonMode = true;
textPrivacy.on('pointerdown', () => {
// pp
});
textPrivacy.anchor.set(0.5, 0.5);
textPrivacy.x = 24;
textPrivacy.y = this.state.app.view.height - 16;
this.state.app.stage.addChild(textPrivacy);
// terms of service
const textTerms = new PIXI.Text('Terms', {
fontFamily: 'Arial',
fontSize: 12,
fill: '#777777',
});
textTerms.interactive = true;
textTerms.buttonMode = true;
textTerms.on('pointerdown', () => {
// tos
});
textTerms.anchor.set(0.5, 0.5);
textTerms.x = 72;
textTerms.y = this.state.app.view.height - 16;
this.state.app.stage.addChild(textTerms);
Puzzle
Now we need to add puzzle with drag and drop. Puzzle will be Sprite instance with interactive and buttonMode set to true. Also we need to bind event listeners to proper methods. And becuase we want to use our captcha on both mobile and pc we must ensure all input methods are supported.
// puzzle
const puzzle = PIXI.Sprite.from('https://i.imgur.com/sNPmMi2.png');
puzzle.anchor.set(0.5, 0.5);
puzzle.alpha = 0.5;
puzzle.interactive = true;
puzzle.buttonMode = true;
puzzle.x = 64;
puzzle.y = this.state.app.view.height / 2;
puzzle.on('mousedown', this.onDragStart)
.on('touchstart', this.onDragStart)
.on('mouseup', this.onDragEnd)
.on('mouseupoutside', this.onDragEnd)
.on('touchend', this.onDragEnd)
.on('touchendoutside', this.onDragEnd)
.on('mousemove', this.onDragMove)
.on('touchmove', this.onDragMove);
this.setState(() => {
return {
puzzle
}
});
this.state.app.stage.addChild(puzzle);
Methods onDragStart, on dragEnd, onDragMove are required in the component class. On drag start, we are setting dragging flag in the component state to true, and on drag end to false. When moving cursor or finger above the canvas, onDragMove method will be fired, so we need to make sure we are dragging when holding puzzle piece. Event for onDragMove contains distance from the previous call. And it may be positive or negative.
onDragStart() {
this.setState(() => {
return {
dragging: true,
};
});
}
onDragEnd() {
this.setState(() => {
return {
dragging: false,
};
});
}
onDragMove(event : any) {
if (this.state.dragging) {
const puzzle = this.state.puzzle;
puzzle.position.x += event.data.originalEvent.movementX;
puzzle.position.y += event.data.originalEvent.movementY;
}
}
With this puzzle, we need to add to our state two more properties and bind three new methods to class::
interface IApp {
app: PIXI.Application,
dragging: boolean,
puzzle: PIXI.Sprite,
}
export class App extends React.Component<any, IApp> {
constructor(props : any) {
super(props);
this.state = {
app: new PIXI.Application({
width: 480,
height: 280,
backgroundColor: 0xeeeeee,
resolution: window.devicePixelRatio || 1,
}),
dragging: false,
puzzle: null
};
this.onDragEnd = this.onDragEnd.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onDragMove = this.onDragMove.bind(this);
}
// ...
}
You should be able to drag the puzzle over the canvas and click on the submit button and small text on the bottom of the canvas.
Congratulations! In the next episode I will explain backend side of the mechanism, so If you want to be notified about the next part, follow me on DEV.to. 😉
Current source code is available on GitHub. Please, leave a star ⭐ if you like project.
pilotpirxie / devcaptcha
🤖 Open source captcha made with React, Node and TypeScript for DEV.to community
devcaptcha
Open source captcha made with React, Node and TypeScript for DEV.to community
Features
- Fast and efficient, uses Redis as temp storage,
- Implements leading zero challenge,
- Requires image recognition to find coordinates on a background,
- Customizable, you can easily tailor to your needs,
- Simple integration in just few minutes,
- Written with Typescript, React, Node and Express,
Getting started
git clone https://github.com/pilotpirxie/devcaptcha.git
cd devcaptcha/devcaptcha-server
yarn install
yarn start
Integration
Captcha should be configured equally on the client and backend side to works correctly.
const devcaptcha = new DevCaptcha({
appendSelector: '#captcha',
promptText: 'Move the puzzle to the correct position to solve captcha',
lockedText: 'Locked',
savingText: 'Wait',
privacyUrl: 'https://example.com',
termsUrl: 'https://example.com',
baseUrl: 'http://localhost:8081',
puzzleAlpha: 0.9,
canvasContainerId: 'devcaptcha-container',
leadingZerosLength: 3,
workerPath: './worker.js'
});
Client Config Definition:
export type CaptchaConfig
…
Top comments (0)