You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Angular角丝带组件的动态尺寸?

动态尺寸角丝带Angular组件实现问题

背景

参考纯CSS角丝带效果,我开发了一个Angular组件。现在需要通过暴露@Input size(单位如px)实现丝带尺寸的动态调整。最初考虑使用CSS transform scale属性,但缩放后需要调整位置以贴合父元素角落,相关数学计算十分繁琐。后来通过calc计算transform-origin的值实现了缩放锚定,现在需要将四个位置的计算公式简化为可返回对应transform-originX、Y值的函数。

当前代码实现

Angular组件代码

import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core';

@Component({
  selector: 'ribbon',
  template: `
  <div class="ribbon ribbon-{{position}}"
       [style.cursor]="isClickable ? 'pointer' : 'default'"
       (click)="this.click.emit()">
    <div class="ribbon-label"><span>{{ label }}</span></div>
  </div>`,
  styleUrls: ['ribbon.component.scss']
})
export class RibbonComponent implements AfterViewInit {
  @Input() position: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left' = 'top-left';
  /** Default value is a No-Break Space so as to not break the ribbon shape when no label is set. */
  @Input() label = '\u00A0';
  /** Can be set to one of the scss color variables defined from the theme.scss
   * or a custom CSS color value such as `rgba` , HEX, HSL, or a CSS predefined color name. */
  @Input() color: 'primary' | 'secondary' | 'warning' | 'danger' | string = 'danger';
  @Output() click = new EventEmitter<void>();

  
  get isClickable(): boolean {
    return !!this.click.observers.length;
  }

  constructor(private renderer: Renderer2, private el: ElementRef) { }

  ngAfterViewInit() {
    // Force parent/container element to have position relative
    // so that the ribbon's offset using position absolute is secured
    const parent = this.el.nativeElement.parentNode;
    this.renderer.setStyle(parent, 'position', 'relative');
  }
}

SCSS样式代码

$primary-color: #3498db; // base color of the ribbon
$secondary-color: darken($primary-color, 10%); // darkened base color for the folded part
$label-color: #fff;

// TODO: Make side of the square dynamic. This can be done by binding transform scale but offset calculation will be tricky
$main-width: 150px; // ribbon is wrapped by a square
$ribbon-width: $main-width * 1.5; // 225px
$ribbon-underside-length: $main-width * 0.033; // ~5px, How much of the underside of the folded ribbon is exposed.
$ribbon-padding: $main-width * 0.1; // ~15px
$label-min-height: $main-width *0.12; // ~18px
$offset: $ribbon-underside-length * -2; // ~-10px

.ribbon {
  width: $main-width;
  height: $main-width;
  overflow: hidden;
  position: absolute;

  &::before,
  &::after {
    position: absolute;
    z-index: -1;
    content: "";
    display: block;
    border: $ribbon-underside-length solid $secondary-color;
  }

  .ribbon-label {
    position: absolute;
    display: block;
    width: $ribbon-width;
    padding: $ribbon-padding 0;
    background-color: $primary-color;
    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
    color: $label-color;
    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
    text-transform: uppercase;
    text-align: center;
    line-height: $label-min-height !important; // lock in so the ribbon's thickness is unaffected by the label's font-size
  }
}

@mixin ribbon-position($positionY, $positionX, $angle) {
  .ribbon-#{$positionY}-#{$positionX} {
    #{$positionY}: $offset; // top or bottom
    #{$positionX}: $offset; // left or right

    // #region folded sides of the ribbon
    &::before,
    &::after {
      border-#{$positionY}-color: transparent;
      border-#{$positionX}-color: transparent;
    }

    &::before {
      #{$positionY}: 0; // top or bottom
      #{if($positionX == 'left', 'right', 'left')}: 0; // anchor on the opposite side
    }

    &::after {
      #{if($positionY == 'top', 'bottom', 'top')}: 0; // anchor on the opposite side
      #{$positionX}: 0; // left or right
    }
    // #endregion

    .ribbon-label {
      #{if($positionX == 'left', 'right', 'left')}: -25px; // offset on opposite side
      #{$positionY}: 30px; // top or bottom
      transform: rotate(#{$angle}deg);

      // Corrective centering of label for top-right and bottom-left ribbon
      @if ($positionY == 'top' and $positionX == 'right' ) or ($positionY == 'bottom' and $positionX == 'left') {
        span {
          padding-left: 1.5ch; // add padding equivalent to 150% width of "0" in current font-size
        }
      }
    }
  }
}

@include ribbon-position(top, left, -45);
@include ribbon-position(top, right, 45);
@include ribbon-position(bottom, left, 225);
@include ribbon-position(bottom, right, -225);

已取得的进展

通过calc计算transform-origin实现了缩放锚定,其中undersideWidth代表丝带折叠部分的外露宽度,amount为缩放值(取值范围0-1)。目前四个位置的计算公式如下:

  • top-left
transform: scale(amount);
transform-origin: calc(0% + (((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth) calc((0% + ((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth);
  • top-right
transform: scale(amount);
transform-origin: calc((100% - ((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth* -1) calc((0% + ((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth);
  • bottom-left
transform: scale(amount);
transform-origin: calc((0% + ((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth) calc(((100% - ((1 - 0.5) * undersideWidth)) - ((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth* -1);
  • bottom-right
transform: scale(amount);
transform-origin: calc((100% - ((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth* -1) calc((100% - ((1 - amount) * undersideWidth)) + (1 + amount) * undersideWidth* -1);

这些公式可按上下/左右位置拆分逻辑:Y轴参考点top-*为0%、bottom-*为100%;X轴参考点*-left为0%、*-right为100%,其余部分基于缩放后的外露宽度计算偏移。现在需要将这些公式简化为可返回对应transform-originX、Y值的函数。


内容的提问来源于stack exchange,提问作者Alex Pappas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 21:02:50