基于 Canvas 的多行文本溢出方案

news2024/11/15 12:31:48

说到文本溢出,大家应该都不陌生,中文网络上的文章翻来覆去就是下面3种方法:

单行文本溢出

这是日常开发中用的最多的,核心代码如下:

p {
  width: 300px;
  overflow: hidden; 
  white-space: nowrap; /*文本不会换行*/
  text-overflow: ellipsis;  /*当文本溢出包含元素时,以省略号表示超出的文本*/
}

但这个方法只对单行文本生效,如果我们想要对多行文本实现溢出控制,那要如何做呢?

多行文本溢出

总的来说,有2种思路,一种是基于 CSS 里的 box-orient(已废弃),另一种是基于伪元素。

基于 box-orient

p {
  width: 300px;
  overflow: hidden; /*将对象作为弹性伸缩盒子模型显示*/
  display: -webkit-box; /*设置子元素排列方式*/
  -webkit-box-orient: vertical; /*设置显示的行数,多出的部分会显示为...*/
  -webkit-line-clamp: 3;
}

这里用到了box-orient这个属性以及webkit-line-clamp,但是这个方法其实是不推荐在生产环境使用的,因为box-orient这个属性现在已经不推荐使用了,详见 box-orient的官方描述

基于伪元素

p {
  position: relative;
  line-height: 1.2em;
  max-height: 3.6em;
  width: 300px; 
  text-align: justify; /*设置文本为两端对齐*/
  overflow: hidden;
}

p::after {
  content: '...';
  position: absolute;
  bottom: 0;
  right: 0; 
  width: 1em; /*将省略号的大小设置为1个字体大小*/
  background: #fff;/*设置背景,将最后一个字覆盖掉*/
}

可以看到这种方法主要是通过在段落的末尾添加1个伪元素,来覆盖最后的文字,但是这种方法无法动态地依据文本的长度来展示溢出元素,所以我们可以在这里做一些 hack。
效果图如下:

动态适应

先上效果:

pseudo-element-overflow

所以如果我们想要实现动态适应,要怎么做呢?这里给出 mxclsh大佬的一种基于float属性的方法(细节见文末的“参考资料”),基本原理:

有个三个盒子 div,粉色盒子左浮动,浅蓝色盒子和黄色盒子右浮动,

  1. 当浅蓝色盒子的高度低于粉色盒子,黄色盒子仍会处于浅蓝色盒子右下方。
  2. 如果浅蓝色盒子文本过多,高度超过了粉色盒子,则黄色盒子不会停留在右下方,而是掉到了粉色盒子下。

那么我们可以将黄色盒子进行相对定位,将内容溢出的黄色盒子移动到文本内容右下角,而未溢出的则会被移到外太空去了。
代码
HTML

<div class="wrap">
    <div class="text">这是一段文字。这是一段文字。这是一段文字。这是一段文字。这是一段文字。这是一段文字。这是一段文字。这是一段文字。这是一段文字。这是一段文字。Lorem ipsum dolor sit
        amet,
        consectetur adipisicing elit. Dignissimos labore sit vel itaque
        delectus atque quos magnam assumenda quod architecto perspiciatis animi.</div>
</div>

CSS

.wrap {
    height: 40px;
    line-height: 20px;
    overflow: hidden;
}

.wrap .text {
    float: right;
    margin-left: -5px;
    width: 100%;
    background-color: rgb(30, 195, 232);
    word-break: break-all;
}

.wrap::before {
    float: left;
    width: 5px;
    content: '';
    height: 40px;
}

.wrap::after {
    float: right;
    content: "...";
    height: 20px;
    line-height: 20px;
    /* 为三个省略号的宽度 */
    width: 3em;
    /* 使盒子不占位置 */
    margin-left: -3em;
    /* 移动省略号位置 */
    position: relative;
    left: 100%;
    top: -20px;
    padding-right: 5px;
    /* White background */
    background-color: rgb(202, 225, 24);
    /* Blur effect */
    backdrop-filter: blur(10px);
}

但是如果我们不仅想要多行文本不仅能做到动态适应,且能做到自定义溢出元素(例如插入1个 emoij 或图片),那该怎么办呢?这个时候我们就要寄出 Canvas 这个大杀器。

基于Canvas来实现多行文本溢出

这里我们需要跳出已有的思维禁锢,考虑用新的思路来做文本截断。

核心:用 canvas 的 measureText 来计算文本的理论最大长度,然后结合指定的最大行数和单行文本的宽度,通过二分算法来找到真正截断应该发生的地方,并展示自定义溢出元素
下面给出伪代码,具体的实现大家可以尽情发挥,这里是有很多可以优化的空间的(づ ̄3 ̄)づ╭❤~

const MagicText = (props: MagicTextProps) => {
  useEffect(() => {
    handleTruncation(textMaxLine, props.elementId!);
  }, [props.style, props.children]);

  return (
    <span data-tag="magic-text" data-element-id={props.elementId} style={props.style}>
      <span style={{ width: '100%' }}>{props.children}</span>
    </span>
  );
};


function handleTruncation(textMaxLine: number, elementId: string) {
  const ele = document.querySelector(`span[data-element-id='${elementId}']`);
  if (!ele) {
    return;
  }

  // check whether "magic-inline-truncation" exists in children. If it does, then we should do truncation
  const nestedChild = ele.children[0].childNodes;
  let inlineTruncationElement;
  Array.from(nestedChild).some((item: any) => {
    if (item.attributes?.['data-tag'].value === 'magic-inline-truncation') {
      inlineTruncationElement = item;
      return true;
    }
  });

  const truncationWidth =
    inlineTruncationElement?.getBoundingClientRect().width ?? 0;
  // if truncationWidth <= 0, then we should not do truncation
  if (truncationWidth <= 0) {
    return;
  }

  //! try to calculate the max width with "magic-inline-truncation"
  // principle:
  //  1. get the width of magic-text
  //  2. if width is not set, get width from its parent
  const widthFromStyle = window.getComputedStyle(ele).width;
  // it can be optimized later
  const lineWidth: number =
    widthFromStyle === ''
      ? Math.floor(ele.getBoundingClientRect().width)
      : Number(widthFromStyle.slice(0, -2));
  const maxLine = textMaxLine == 0 ? 1 : textMaxLine;
  const maxTotalWidth = Math.floor(lineWidth * maxLine); // get the maximum width
  const content = String(ele.children[0].childNodes[0].textContent); // read the text content
  const textStyle = getCanvasFont(ele);
  const totalTextWidth = getTextWidth(content, textStyle); // calculate the text width with canvas
  const targetTotalWidth = maxTotalWidth - truncationWidth; // the expected width
  if (totalTextWidth >= maxTotalWidth) {
    // try to do binary search to find the right text
    const newContent = binarySearch(
      content.split(''),
      targetTotalWidth,
      textStyle
    );
    nestedChild[0].nodeValue = newContent;
  } else {
    // hide the truncation
    inlineTruncationElement.style.display = 'none';
  }
}

// Try to find the exact position in the text where the truncation should start
function binarySearch(
  text: string[],
  targetWidth: number,
  textStyle: string
): string {
  let left = 0;
  let right = text.length - 1;
  const DELTA_WIDTH = 5; // It represents the width of single character and it use to judge critical conditions

  while (left <= right) {
    const mid = Math.floor(left + (right - left) / 2);
    const searchWidthText = text.slice(0, mid + 1).join('');
    const textWidth = getTextWidth(searchWidthText, textStyle);
    if (isHitTarget(targetWidth, textWidth, DELTA_WIDTH)) {
      return searchWidthText;
    } else if (textWidth < targetWidth) {
      left = mid + 1;
    } else if (textWidth > targetWidth) {
      right = mid - 1;
    }
  }

  return text.join('');
}

function isHitTarget(target: number, source: number, delta: number) {
  return Math.abs(target - source) <= delta;
}


interface MagicTextProps {
  /**
   * maximum number of lines for text
   */
  'text-maxline'?: string;

  /**
   * The logic of text truncation when text overflows
   * clip: directly truncate
   * tail: add ellipsis to the end
   */
  'ellipsize-mode'?: 'clip' | 'tail';
}

计算文本具体有多宽的核心代码如下:

/**
 * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
 *
 * @param {String} text The text to be rendered.
 * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
 *
 */
function getTextWidth(text: string, font: string): number {
  // re-use canvas object for better performance
  let canvas;
  if (getTextWidth.prototype.canvas) {
    canvas = getTextWidth.prototype.canvas;
  } else {
    canvas = document.createElement('canvas');
  }
  const context = canvas.getContext('2d');
  context.font = font;
  const metrics = context.measureText(text);
  return metrics.width;
}

function getCssStyle(element: Element, prop: string) {
  return window.getComputedStyle(element, null).getPropertyValue(prop);
}

// currently, we calculate text width using only "font-size", "font-family", and "font-weight", but
// we can consider more styles that impact text width later on
function getCanvasFont(el: Element = document.body): string {
  const fontWeight =
    getCssStyle(el, 'font-weight') || getCssStyle(document.body, 'normal');
  const fontSize =
    getCssStyle(el, 'font-size') || getCssStyle(document.body, 'font-size');
  const fontFamily =
    getCssStyle(el, 'font-family') || getCssStyle(document.body, 'font-family');

  return `${fontWeight} ${fontSize} ${fontFamily}`;
}

总结

几种方式的优缺点和特点如下:

text-overflow伪元素伪元素+float基于Canvas
支持单行文本溢出
支持多行文本溢出
支持自适应
支持自定义溢出的元素
支持自定义最大行数
性能一般

参考资料

https://blog.csdn.net/mxclsh/article/details/84250007
https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1369432.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于模块自定义扩展字段的后端逻辑实现(二)

目录 一&#xff1a;创建表 二&#xff1a;代码逻辑 上一节我们详细讲解了自定义扩展字段的逻辑实现和表的设计&#xff0c;这一节我们以一个具体例子演示下&#xff0c;如何实现一个订单模块的自定义扩展数据。 一&#xff1a;创建表 订单主表: CREATE TABLE t_order ( …

rime中州韵小狼毫 中英互绎 滤镜

英文在日常生活中已经随处可见&#xff0c;我们一般中英互译需要使用专业的翻译软件来实现。但如果我们在输入法中&#xff0c;在输入中文的时候&#xff0c;可以顺便瞟一眼对应的英文词汇&#xff0c;或者在输入英文的时候可以顺便了解对应的中文词汇&#xff0c;那将为我们的…

1.8 day6 IO进程线程

使用有名管道实现两个进程之间的通信 进程A #include <myhead.h> int main(int argc, const char *argv[]) {//创建两个文件描述符用于打开两个管道int fd1-1;int fd2-1;//创建一个子进程int pid-1;if((fd1open("./mkfifo1",O_RDWR))-1){perror("open er…

OpenAI ChatGPT-4开发笔记2024-03:Chat之Tool和Tool_Call(含前function call)

Updates on Function Calling were a major highlight at OpenAI DevDay. In another world,原来的function call都不再正常工作了&#xff0c;必须全部重写。 function和function call全部由tool和tool_choice取代。2023年11月之前关于function call的代码都准备翘翘。 干嘛…

小梅哥Xilinx FPGA学习笔记22——ip核之FIFO

目录 一&#xff1a;章节说明 1.1 FIFO IP简介 1.2 FIFO Generato IP 核信号框图 1.3 实验任务 二&#xff1a;FIFO 写模块设计 2.1 简介 2.2 模块框图 2.3 模块端口与功能描述 2.4 写模块代码 三 FIFO 读模块设计 3.1 简介 3.2 模块框图 3.3 模块端口与功…

【自学笔记】01Java基础-07面向对象基础-03常量、枚举类、抽象类、多态详解

记录java基础学习中有关常量、枚举类、抽象类和多态的内容。 1 常量 什么是常量&#xff1f; 常量是使用了public static final修饰的成员变量&#xff0c;必须有初始化值&#xff0c;而且执行的过程中其值不能被改变。 常量名的命名规范&#xff1a;英文单词全部大写&#x…

Transformer架构的局限已凸显,被取代还有多久?

江山代有才人出&#xff0c;各领风骚数百年。这句话无论是放在古往今来的人类身上&#xff0c;还是放在当今人工智能领域的大模型之上&#xff0c;都是最贴切不过的。无论是一个时代的伟人&#xff0c;还是统治一个领域的技术&#xff0c;最终都会有新的挑战者将其替代。Transf…

springBoot容器功能

一、添加组件 1、Configuration 1.1基本使用 新建一个MyConfig类 , 演示Configuration Bean的作用 &#xff0c; 即相当于spring中的beanx.xml&#xff0c; Bean 就是bean标签 此方法&#xff0c;默认是单实例&#xff0c; 即获取多少次都是同一个对象 自定义名字&#xff0…

令人绝望的固化和突破-2024-

这是继续写给自己求生之路的记录。 所有成熟稳定的行业都是相对固化的&#xff0c;上升通道及其严苛。 博客 我刚写博客的2015-2017这3年&#xff0c;其实还能带动一些学生&#xff0c;然后部分学生心中有火&#xff0c;眼里有光&#xff0c;也有信心自己做好&#xff0c;还有…

利用“与非”运算实现布尔代数中的与,或,非三种运算

什么是“与非”运算&#xff1f; 要想明白“与非”运算&#xff0c;首先要明白“与”运算和“非”运算。 “与”运算在离散数学中叫做合取式&#xff0c;也就是A和B相同时为1的时候结果才为1&#xff0c;其余情况都为0 下面是“与”运算的真值表 “非”运算在离散数学中叫做否…

2023年阿里云云栖大会:前沿技术发布与未来展望

在2023年的阿里云云栖大会上&#xff0c;我见证了云计算和人工智能领域的又一历史性时刻。这次大会不仅是对未来科技趋势的一次深入探索&#xff0c;更是阿里云技术实力和创新能力的集中展示。 首先&#xff0c;千亿级参数规模的大模型通义千问2.0的发布&#xff0c;无疑将人工…

实战演练 | Navicat 中编辑器设置的配置

Navicat 是一款功能强大的数据库管理工具&#xff0c;为开发人员和数据库管理员提供稳健的环境。其中&#xff0c;一个重要功能是 SQL 编辑器&#xff0c;用户可以在 SQL 编辑器中编写和执行 SQL 查询。Navicat 的编辑器设置可让用户自定义编辑器环境&#xff0c;以满足特定的团…

软件测试|MySQL逻辑运算符使用详解

简介 在MySQL中&#xff0c;逻辑运算符用于处理布尔类型的数据&#xff0c;进行逻辑判断和组合条件。逻辑运算符主要包括AND、OR、NOT三种&#xff0c;它们可以帮助我们在查询和条件语句中进行复杂的逻辑操作。本文将详细介绍MySQL中逻辑运算符的使用方法和示例。 AND运算符 …

GPT Prompts Hub:2024年最新ChatGPT提示词项目,革新对话结构!

&#x1f31f; GPT Prompts Hub &#x1f31f; 欢迎来到 “GPT Prompts Hub” 存储库&#xff01;探索并分享高质量的 ChatGPT 提示词。培养创新性内容&#xff0c;提升对话体验&#xff0c;激发创造力。我们极力鼓励贡献独特的提示词。 在 “GPT Prompts Hub” 项目中&#…

解决不同请求需要的同一实体类参数不同(分组校验validation)

问题概述 新增目录是自动生成id&#xff0c;不需要id参数&#xff1b;更新目录需要id&#xff0c;不能为空 pom.xml中已有spring-boot-starter-validation依赖 <!--validation(完成属性限制&#xff0c;参数校验)--><dependency><groupId>org.springframew…

【C语言题解】 | 101. 对称二叉树

101. 对称二叉树 101. 对称二叉树代码 101. 对称二叉树 这个题目要求判断该二叉树是否为对称二叉树&#xff0c;此题与上一题&#xff0c;即 100. 相同的树 这个题有异曲同工之妙&#xff0c;故此题可借鉴上题。 我们先传入需要判断二叉树的根节点&#xff0c;通过isSameTree()…

赠送葡萄酒:为别人选择合适的葡萄酒

葡萄酒可以在许多不同的场合成为很好的礼物&#xff0c;因为它可以用来庆祝许多不同的事情。当被邀请去别人家时&#xff0c;你可以带酒去吃饭。葡萄酒可以用来纪念婚礼、出生、毕业和各种纪念日&#xff0c;来自云仓酒庄品牌雷盛红酒分享这是一个非常合适的专业礼物。但是你怎…

1876_电感的特性小结

Grey 全部学习内容汇总&#xff1a; GitHub - GreyZhang/g_hardware_basic: You should learn some hardware design knowledge in case hardware engineer would ask you to prove your software is right when their hardware design is wrong! 1876_电感的特性小结 主要是…

[开源]万界星空开源MES系统,支持低代码大屏设计

一、开源系统概述&#xff1a; 万界星空科技免费MES、开源MES、商业开源MES、商业开源低代码MES、市面上最好的开源MES、MES源代码、免费MES、免费智能制造系统、免费排产系统、免费排班系统、免费质检系统、免费生产计划系统、精美的数据大屏。 二、开源协议&#xff1a; 使…

中央处理器CPU(1)----指令周期和微程序

前言&#xff1a;由于期末复习计算机组成效率太慢所以抽时间写一下文章总结一下思路&#xff0c;理解不是很深&#xff0c;欢迎各位不吝赐教。 由于时间不是很充分&#xff0c;所以有些考点由于我们不考试&#xff0c;一笔带过了。 我这是期末复习总结&#xff0c;不是考研知识…