UNPKG

react-native-runescape-text

Version:

Convert text to a text GIF image with RuneScape chat effects for React Native.

360 lines (312 loc) 11.8 kB
const { Context, measureText } = require("./Context"); const {gradient2FillStyle, isAnimated} = require("../utils/effectUtil"); const range = require("../utils/range"); const {getImageDataSxOrSy} = require("../utils/configUtil"); class Motion { constructor(config) { this.version = config.version; this.scale = config.scale; this.fontName = config.font; this.totalFrames = config.totalFrames; this.clipWidth = config.width; this.clipHeight = config.height; this.verticalText = config.verticalText; this.imageSmoothingEnabled = config.imageSmoothingEnabled; this.imageGradientEnabled = config.imageGradientEnabled; this.gradientThreshold = config.gradientThreshold; } renderNoneStatic(message, color) { const textArray = message.split('\n'); const isMultiline = textArray.length > 1; const { width: firstWidth, height: firstHeight, ascent } = measureText(textArray[0], this.scale, this.fontName); const verticalSpacingRatio = 0.85; let oldWidth = firstWidth; if (isMultiline) { textArray.map(text => { const {width} = measureText(text, this.scale, this.fontName); if (width > oldWidth) { oldWidth = width; } }) } else if (this.verticalText) { oldWidth = firstHeight; } let oldHeight = firstHeight; if (isMultiline) { oldHeight = firstHeight * verticalSpacingRatio * textArray.length + firstHeight * (1 - verticalSpacingRatio); } else if (this.verticalText) { oldHeight = firstHeight * verticalSpacingRatio * message.length + firstHeight * (1 - verticalSpacingRatio); } const context = new Context( oldWidth, oldHeight, this.scale, this.imageSmoothingEnabled, this.fontName, ).getStatic(); context.fillStyle = color.calculate(0); if (this.verticalText) { context.textAlign = "center"; for (let i = 0; i < message.length; i++) { context.fillText(message[i], oldWidth / 2, ascent + i * firstHeight * verticalSpacingRatio); } } else { // textAlign default is "start" after getContext("2d") for (let i = 0; i < textArray.length; i++) { context.fillText(textArray[i], 0, ascent + i * firstHeight * verticalSpacingRatio); } } let clipWidth = this.clipWidth; if (isMultiline || !this.clipWidth) { clipWidth = oldWidth; } let clipHeight = this.clipHeight; if (isMultiline || !this.clipHeight) { clipHeight = oldHeight; } let sx = clipWidth ? getImageDataSxOrSy(oldWidth, clipWidth) : 0; let sy = clipHeight ? getImageDataSxOrSy(oldHeight, clipHeight) : 0; let sw = clipWidth ? clipWidth : oldWidth; let sh = clipHeight ? clipHeight : oldHeight; let imageData = context.getImageData(sx, sy, sw, sh); if (!this.imageGradientEnabled) { gradient2FillStyle(imageData, context.fillStyle, this.gradientThreshold); } return [imageData]; } renderNoneDynamic(line, color) { const { width, height, ascent } = measureText(line, this.scale, this.fontName); return range(this.totalFrames).map((frame) => { const context = new Context( width, height, this.scale, this.imageSmoothingEnabled, this.fontName, ).getDynamic(); context.fillStyle = color.calculate(frame); context.fillText(line, 0, ascent); let sx = this.clipWidth ? getImageDataSxOrSy(width, this.clipWidth) : 0; let sy = this.clipHeight ? getImageDataSxOrSy(height, this.clipHeight) : 0; let sw = this.clipWidth ? this.clipWidth : width; let sh = this.clipHeight ? this.clipHeight : height; let imageData = context.getImageData(sx, sy, sw, sh); if (!this.imageGradientEnabled) { gradient2FillStyle(imageData, context.fillStyle, this.gradientThreshold); } return imageData; }); } getWave() { return { amplitudeFactor: 1 / 3, getTotalWidth: (width) => width, frameFactor: 1, getWave: (wave) => 1 + wave, getX: (width) => width, }; } getWave2() { return { amplitudeFactor: 1 / 6, getTotalWidth: (width, amplitude) => Math.round(width + 2 * amplitude), frameFactor: 1, getWave: (wave) => 1 + wave, getX: (width, displacement) => Math.round(width + displacement), }; } getShake() { return { amplitudeFactor: 1 / 3, getTotalWidth: (width) => width, frameFactor: 6, getWave: (wave, frame) => 2 * frame > this.totalFrames ? 1 : 1 + wave * (1 - (2 * frame) / this.totalFrames), getX: (width) => width, }; } renderWave( line, color, { amplitudeFactor, getTotalWidth, frameFactor, getWave, getX } ) { const { width, height, ascent } = measureText(line, this.scale, this.fontName); const amplitude = height * amplitudeFactor; const totalWidth = getTotalWidth(width, amplitude); const totalHeight = Math.round(height + 2 * amplitude); return range(this.totalFrames).map((frame) => { const context = new Context( totalWidth, totalHeight, this.scale, this.imageSmoothingEnabled, this.fontName, ).getDynamic(); context.fillStyle = color.calculate(frame); line.split("").forEach((char, index) => { const wave = Math.sin( Math.PI * (index / 6 + 8 * frameFactor * (frame / this.totalFrames)) ); const displacement = amplitude * getWave(wave, frame); const x = getX( measureText(line.slice(0, index), this.scale, this.fontName).width, displacement ); const y = Math.round(ascent + displacement); context.fillText(char, x, y); }); let sx = this.clipWidth ? getImageDataSxOrSy(width, this.clipWidth) : 0; let sy = this.clipHeight ? getImageDataSxOrSy(height, this.clipHeight) : 0; let sw = this.clipWidth ? this.clipWidth : width; let sh = this.clipHeight ? this.clipHeight : height; let imageData = context.getImageData(sx, sy, sw, sh); if (!this.imageGradientEnabled) { gradient2FillStyle(imageData, context.fillStyle, this.gradientThreshold); } return imageData; }); } renderScroll(line, color) { const { width, height, ascent } = measureText(line, this.scale, this.fontName); return range(this.totalFrames).map((frame) => { const context = new Context( width, height, this.scale, this.imageSmoothingEnabled, this.fontName, ).getDynamic(); context.fillStyle = color.calculate(frame); const displacement = width - ((2 * frame) / this.totalFrames) * width; context.fillText(line, Math.round(displacement), ascent); let sx = this.clipWidth ? getImageDataSxOrSy(width, this.clipWidth) : 0; let sy = this.clipHeight ? getImageDataSxOrSy(height, this.clipHeight) : 0; let sw = this.clipWidth ? this.clipWidth : width; let sh = this.clipHeight ? this.clipHeight : height; let imageData = context.getImageData(sx, sy, sw, sh); if (!this.imageGradientEnabled) { gradient2FillStyle(imageData, context.fillStyle, this.gradientThreshold); } return imageData; }); } getSlideOsrs() { return { getY: (ascent, frame, motionFrameIndex, height) => { let displacement = ascent; if (frame < motionFrameIndex) { displacement -= ((motionFrameIndex - frame) / motionFrameIndex) * height; } else if (frame > this.totalFrames - motionFrameIndex) { displacement -= ((this.totalFrames - motionFrameIndex - frame) / motionFrameIndex) * height; } return Math.round(displacement); }, }; } getSlideRs3() { return { getY: (ascent, frame, motionFrameIndex, height) => { let displacement = ascent; if (frame < motionFrameIndex) { displacement += ((motionFrameIndex - frame) / motionFrameIndex) * height; } else if (frame > this.totalFrames - motionFrameIndex) { displacement += ((this.totalFrames - motionFrameIndex - frame) / motionFrameIndex) * height; } return Math.round(displacement); }, }; } renderSlide(line, color, { getY }) { const { width, height, ascent } = measureText(line, this.scale, this.fontName); const motionFrameIndex = Math.round(this.totalFrames / 6); return range(this.totalFrames).map((frame) => { const context = new Context( width, height, this.scale, this.imageSmoothingEnabled, this.fontName, ).getDynamic(); context.fillStyle = color.calculate(frame); context.fillText(line, 0, getY(ascent, frame, motionFrameIndex, height)); let sx = this.clipWidth ? getImageDataSxOrSy(width, this.clipWidth) : 0; let sy = this.clipHeight ? getImageDataSxOrSy(height, this.clipHeight) : 0; let sw = this.clipWidth ? this.clipWidth : width; let sh = this.clipHeight ? this.clipHeight : height; let imageData = context.getImageData(sx, sy, sw, sh); if (!this.imageGradientEnabled) { gradient2FillStyle(imageData, context.fillStyle, this.gradientThreshold); } return imageData; }); } mergeImageDatas(mergedImageDatas, imageDatas) { if (mergedImageDatas.length === 0) { return imageDatas; } const maxWidth = Math.max( mergedImageDatas[0].width, imageDatas[0].width ); const totalHeight = mergedImageDatas[0].height + imageDatas[0].height; return mergedImageDatas.map((imageData, index) => { const newContext = new Context( maxWidth, totalHeight, this.scale, this.imageSmoothingEnabled, this.fontName, ).getMerge(); newContext.drawImage(imageData, 0, 0); newContext.drawImage(imageDatas[index], 0, imageData.height); let sx = this.clipWidth ? getImageDataSxOrSy(width, this.clipWidth) : 0; let sy = this.clipHeight ? getImageDataSxOrSy(height, this.clipHeight) : 0; let sw = this.clipWidth ? this.clipWidth : width; let sh = this.clipHeight ? this.clipHeight : height; return newContext.getImageData(sx, sy, sw, sh); }); } setMotion(motion) { const motionFunctionMap = { osrs: { none: this.renderNoneDynamic, wave: (line, color) => this.renderWave(line, color, this.getWave()), wave2: (line, color) => this.renderWave(line, color, this.getWave2()), shake: (line, color) => this.renderWave(line, color, this.getShake()), scroll: this.renderScroll, slide: (line, color) => this.renderSlide(line, color, this.getSlideOsrs()), }, rs3: { none: this.renderNoneDynamic, wave: (line, color) => this.renderWave(line, color, this.getWave()), wave2: (line, color) => this.renderWave(line, color, this.getWave2()), shake: (line, color) => this.renderWave(line, color, this.getShake()), scroll: this.renderScroll, slide: (line, color) => this.renderSlide(line, color, this.getSlideRs3()), }, }; this.motion = motion; this.motionFunction = motionFunctionMap[this.version][this.motion]; } render(message, color) { if (!isAnimated(color.color, this.motion)) { return this.renderNoneStatic(message, color); } return message.split("\n").reduce((mergedImageDatas, line) => { const imageDatas = this.motionFunction(line, color); return this.mergeImageDatas(mergedImageDatas, imageDatas); }, []); } } module.exports = Motion;