Skip to content

Commit

Permalink
feat: added support for multiline text in canvas mask text options
Browse files Browse the repository at this point in the history
  • Loading branch information
matteobruni committed Oct 14, 2022
1 parent 2ec03cc commit eceacbe
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 14 deletions.
83 changes: 83 additions & 0 deletions demo/vanilla/public/presets/textMaskMultiline.json
@@ -0,0 +1,83 @@
{
"smooth": true,
"interactivity": {
"events": {
"onHover": {
"enable": true,
"mode": "bubble",
"parallax": {
"enable": false,
"force": 2,
"smooth": 10
}
}
},
"modes": {
"bubble": {
"distance": 40,
"duration": 2,
"opacity": 8,
"size": 15
}
}
},
"particles": {
"move": {
"direction": "none",
"distance": 2,
"enable": true,
"outModes": "out",
"speed": 1
},
"number": {
"value": 600
},
"color": {
"value": "random"
},
"shape": {
"type": [
"circle",
"square",
"triangle"
]
},
"size": {
"value": {
"min": 1,
"max": 3
}
}
},
"canvasMask": {
"enable": true,
"override": {
"color": false
},
"scale": 1,
"pixels": {
"filter": "pixelTextFilter"
},
"position": {
"x": 70,
"y": 30
},
"text": {
"color": "#ff0000",
"font": {
"size": 500
},
"text": "Hello\nWorld",
"lines": {
"spacing": 50
}
}
},
"background": {
"color": "#000000",
"image": "",
"position": "50% 50%",
"repeat": "no-repeat",
"size": "cover"
}
}
1 change: 1 addition & 0 deletions demo/vanilla/views/index.pug
Expand Up @@ -133,6 +133,7 @@ html(lang="en")
option(value="svgReplace") Svg Replace
option(value="test") Test
option(value="textMask") Text Mask
option(value="textMaskMultiline") Text Mask Multiline
option(value="trail") Trails
option(value="twinkle") Twinkle
option(value="vibrate") Vibrate
Expand Down
8 changes: 7 additions & 1 deletion plugins/canvasMask/src/CanvasMaskInstance.ts
Expand Up @@ -39,7 +39,13 @@ export class CanvasMaskInstance implements IContainerPlugin {
} else if (options.text) {
const textOptions = options.text;

pixelData = getTextData(textOptions.text, textOptions.color, offset, textOptions.font);
const data = getTextData(textOptions, offset);

if (!data) {
return;
}

pixelData = data;
} else if (options.element || options.selector) {
const canvas =
options.element || (options.selector && document.querySelector<HTMLCanvasElement>(options.selector));
Expand Down
4 changes: 4 additions & 0 deletions plugins/canvasMask/src/Options/Classes/TextMask.ts
@@ -1,15 +1,18 @@
import type { IOptionLoader, RecursivePartial } from "tsparticles-engine";
import { FontTextMask } from "./FontTextMask";
import type { ITextMask } from "../Interfaces/ITextMask";
import { TextMaskLine } from "./TextMaskLine";

export class TextMask implements ITextMask, IOptionLoader<ITextMask> {
color;
font;
lines;
text;

constructor() {
this.color = "#000000";
this.font = new FontTextMask();
this.lines = new TextMaskLine();
this.text = "";
}

Expand All @@ -23,6 +26,7 @@ export class TextMask implements ITextMask, IOptionLoader<ITextMask> {
}

this.font.load(data.font);
this.lines.load(data.lines);

if (data.text !== undefined) {
this.text = data.text;
Expand Down
26 changes: 26 additions & 0 deletions plugins/canvasMask/src/Options/Classes/TextMaskLine.ts
@@ -0,0 +1,26 @@
import type { IOptionLoader, RecursivePartial } from "tsparticles-engine";
import type { ITextMaskLine } from "../Interfaces/ITextMaskLine";

export class TextMaskLine implements ITextMaskLine, IOptionLoader<ITextMaskLine> {
separator: string;
spacing: number;

constructor() {
this.separator = "\n";
this.spacing = 10;
}

load(data?: RecursivePartial<ITextMaskLine> | undefined): void {
if (!data) {
return;
}

if (data.separator !== undefined) {
this.separator = data.separator;
}

if (data.spacing !== undefined) {
this.spacing = data.spacing;
}
}
}
2 changes: 2 additions & 0 deletions plugins/canvasMask/src/Options/Interfaces/ITextMask.ts
@@ -1,7 +1,9 @@
import type { IFontTextMask } from "./IFontTextMask";
import type { ITextMaskLine } from "./ITextMaskLine";

export interface ITextMask {
color: string;
font: IFontTextMask;
lines: ITextMaskLine;
text: string;
}
4 changes: 4 additions & 0 deletions plugins/canvasMask/src/Options/Interfaces/ITextMaskLine.ts
@@ -0,0 +1,4 @@
export interface ITextMaskLine {
separator: string;
spacing: number;
}
58 changes: 45 additions & 13 deletions plugins/canvasMask/src/utils.ts
Expand Up @@ -7,14 +7,21 @@ import type {
RecursivePartial,
} from "tsparticles-engine";
import type { ICanvasMaskOverride } from "./Options/Interfaces/ICanvasMaskOverride";
import type { IFontTextMask } from "./Options/Interfaces/IFontTextMask";
import type { TextMask } from "./Options/Classes/TextMask";

export type CanvasPixelData = {
height: number;
pixels: IRgba[][];
width: number;
};

type TextLineData = {
height: number;
measure: TextMetrics;
text: string;
width: number;
};

export function shuffle<T>(array: T[]): T[] {
for (let currentIndex = array.length - 1; currentIndex >= 0; currentIndex--) {
const randomIndex = Math.floor(Math.random() * currentIndex);
Expand Down Expand Up @@ -148,26 +155,51 @@ export function getImageData(src: string, offset: number): Promise<CanvasPixelDa
return p;
}

export function getTextData(text: string, color: string, offset: number, font: IFontTextMask): CanvasPixelData {
export function getTextData(textOptions: TextMask, offset: number): CanvasPixelData | undefined {
const canvas = document.createElement("canvas"),
context = canvas.getContext("2d");
context = canvas.getContext("2d"),
{ font, text, lines: linesOptions, color } = textOptions;

if (!context) {
throw new Error("Could not get canvas context");
if (!text || !context) {
return;
}

const fontSize = typeof font.size === "number" ? `${font.size}px` : font.size;
const lines = text ? text.split(linesOptions.separator) : "",
fontSize = typeof font.size === "number" ? `${font.size}px` : font.size,
linesData: TextLineData[] = [];

let maxWidth = 0,
totalHeight = 0;

for (const line of lines) {
context.font = `${font.style || ""} ${font.variant || ""} ${font.weight || ""} ${fontSize} ${font.family}`;

context.font = `${font.style || ""} ${font.variant || ""} ${font.weight || ""} ${fontSize} ${font.family}`;
const measure = context.measureText(text),
lineData = {
measure,
text: line,
height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,
width: measure.width,
};

maxWidth = Math.max(maxWidth || 0, lineData.width);
totalHeight += lineData.height + linesOptions.spacing;

linesData.push(lineData);
}

canvas.width = maxWidth;
canvas.height = totalHeight;

const measure = context.measureText(text);
let currentHeight = 0;

canvas.width = measure.width;
canvas.height = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
for (const line of linesData) {
context.font = `${font.style || ""} ${font.variant || ""} ${font.weight || ""} ${fontSize} ${font.family}`;
context.fillStyle = color;
context.fillText(line.text, 0, currentHeight + line.measure.actualBoundingBoxAscent);

context.font = `${font.style || ""} ${font.variant || ""} ${font.weight || ""} ${fontSize} ${font.family}`;
context.fillStyle = color;
context.fillText(text, 0, measure.actualBoundingBoxAscent);
currentHeight += line.height + linesOptions.spacing;
}

return getCanvasImageData(context, canvas, offset);
}
Expand Down

0 comments on commit eceacbe

Please sign in to comment.