zoukankan      html  css  js  c++  java
  • 拖动缩放功能

    需求背景:选一张海报图片,在海报图片上实现自定义填充文字内容,文字颜色和字体可自定义,且文字区域可以拖动和等比例缩放,最终将文字和海报合成一张新的图片

    需要用到的插件:html2canvas(用于将一段html转成canvas)

    下面附上整个组件功能代码:

    /* eslint-disable camelcase */
    import React from 'react'
    import cx from 'classnames'
    import {connect} from 'react-redux'
    import PT from 'prop-types'
    import html2canvas from 'html2canvas'
    import AsyncLottie from '../../../../components/AsyncLottie'
    import lottie from './loading.json'
    import {showToast} from '../../../../utils/zyHybrid'
    import {getImgUrl} from '../../../../utils'
    import {TAB_PANEL, COLOR_SELECTOR, STROKE_COLOR, GREY_STROKE} from './constants'
    import TextInput from './components/textInput'
    import bgUrl from '../images/example1.png'
    import './index.scss'
    
    const lottieOpts = {
      loop: true,
      autoplay: true,
      rendererSettings: {
        preserveAspectRatio: 'xMidYMid slice',
      },
    }
    
    class PosterEditorTool extends React.Component {
      timer = null
      resizeRef = null
      dragRef = null
      isDraging = false
      isResizing = false
    
      state = {
        toCanvas: false,
        currentTabValue: 0,
        img: {
          id: ENVIRONMENT === 'production' ? 1398831582 : 1820587098,
          type: 'img',
        },
        showFontWrap: true,
        showTextInputCom: false,
        fontWrapContent: '活动主题',
        currentFontColor: '#FFFFFF',
        currentFontFamily: '',
        fontFamilySelector: [
          {family: '', showLoading: false, showDownload: false},
          {family: 'PangMenZhengDao', showLoading: false, showDownload: true},
          {family: 'SourceHanSansCN-Bold', showLoading: false, showDownload: true},
          {family: 'zcool-gdh', showLoading: false, showDownload: true},
          {family: 'ZCOOL_KuHei', showLoading: false, showDownload: true},
          {
            family: 'SourceHanSerifCN-Heavy',
            showLoading: false,
            showDownload: true,
          },
          {
            family: 'jiangxizhuokai-Regular',
            showLoading: false,
            showDownload: true,
          },
          {family: 'HappyZcool-2016', showLoading: false, showDownload: true},
          {family: 'JiangChengYuanTi-500W', showLoading: false, showDownload: true},
          {family: 'zcoolwenyiti', showLoading: false, showDownload: true},
          {family: 'xiaowei', showLoading: false, showDownload: true},
          {
            family: 'HanaMinPlus',
            showLoading: false,
            showDownload: true,
          },
        ],
        touchPos: {x: 0, y: 0},
        dragContainerRefPos: {x: 0, y: 0},
        textFontSize: 24,
      }
    
      componentDidMount() {
        this.showFontInit()
      }
    
      componentWillUnmount() {
        const {resizeRef, dragRef} = this
        document.removeEventListener('touchmove', this.handleTouchMove)
        document.removeEventListener('touchend', this.handleTouchEnd)
        if (dragRef) {
          dragRef.removeEventListener('touchstart', this.handleDragRefTouchStart)
        }
        if (resizeRef) {
          resizeRef.removeEventListener(
            'touchstart',
            this.handleResizeRefTouchStart
          )
        }
      }
    
      handleDragRefTouchStart = evt => {
        this.isDraging = true
        this.setState({
          touchPos: {
            x: evt.touches[0].pageX,
            y: evt.touches[0].pageY,
          },
          dragContainerRefPos: {
            x: parseInt(this.dragRef.style.left, 10),
            y: parseInt(this.dragRef.style.top, 10),
          },
        })
      }
    
      handleTouchMove = evt => {
        const {isDraging, isResizing, dragRef, canvasRef} = this
        const {touchPos, dragContainerRefPos, textFontSize} = this.state
        if (isDraging) {
          const tempTop =
            parseInt(dragContainerRefPos.y, 10) +
            parseInt(evt.touches[0].pageY, 10) -
            parseInt(touchPos.y, 10)
          dragRef.style.left = `${dragContainerRefPos.x +
            evt.touches[0].pageX -
            touchPos.x}px`
          dragRef.style.top = `${dragContainerRefPos.y +
            evt.touches[0].pageY -
            touchPos.y}px`
          if (tempTop < 0) {
            dragRef.style.top = '0px'
          }
          if (tempTop > canvasRef.offsetHeight - dragRef.offsetHeight) {
            dragRef.style.top = `${canvasRef.offsetHeight - dragRef.offsetHeight}px`
          }
        }
        if (isResizing) {
          let fontSize = textFontSize + (evt.touches[0].pageX - touchPos.x) * 0.1
          if (fontSize < 10) {
            fontSize = 10
          } else if (fontSize > 80) {
            fontSize = 80
          }
          dragRef.style.fontSize = `${fontSize}px`
        }
      }
    
      handleTouchEnd = () => {
        this.isDraging = false
        this.isResizing = false
      }
    
      handleResizeRefTouchStart = evt => {
        evt.stopPropagation()
        this.isResizing = true
        this.setState({
          textFontSize: parseFloat(this.dragRef.style.fontSize),
          touchPos: {
            x: evt.touches[0].pageX,
            y: evt.touches[0].pageY,
          },
        })
      }
    
      handleCloseFont = () => {
        this.setState({
          showFontWrap: false,
          fontWrapContent: '活动主题',
          textFontSize: '24px',
        })
      }
    
      showFontInit = () => {
        const {resizeRef, dragRef} = this
        dragRef.style.fontSize = '24px'
        dragRef.style.top = '0px'
        dragRef.style.left = '0px'
        document.addEventListener('touchmove', this.handleTouchMove)
        document.addEventListener('touchend', this.handleTouchEnd)
        if (dragRef) {
          dragRef.addEventListener('touchstart', this.handleDragRefTouchStart)
        }
        if (resizeRef) {
          resizeRef.addEventListener('touchstart', this.handleResizeRefTouchStart)
        }
      }
    
      handleConfirm = value => {
        this.setState({
          showTextInputCom: false,
          fontWrapContent: value,
        })
      }
    
      setDefaultImg = id => {
        const img = {id, type: 'img'}
        this.setState({
          img,
        })
      }
    
      uploadImg = () => {
        window.ZuiyouJSBridge.callHandler(
          'uploadFile',
          {
            count: 1,
            file_type: 'img',
            edit: true,
            multiple: false,
            clip_scale: 3 / 1,
          },
          data => {
            const {
              list: [img],
              ret,
              errmsg,
            } = data
            // ios 返回 ret 为 "1"
            if (Number(ret) !== 1) {
              showToast(errmsg || '上传图片异常,请重试~')
              return
            }
            this.setState({
              img,
            })
          }
        )
      }
    
      handleClickComplete = () => {
        const {onComplete} = this.props
        this.setState({toCanvas: true}, () => {
          html2canvas(this.canvasRef, {
            logging: false,
            useCORS: true,
          }).then(canvas => {
            const imgUrl = canvas.toDataURL('image/png')
            onComplete(imgUrl)
          })
        })
      }
    
      changeTab = value => {
        this.setState({
          currentTabValue: value,
        })
      }
    
      handleSelectColor = color => {
        this.setState({currentFontColor: color})
        const {showFontWrap} = this.state
        if (!showFontWrap) {
          this.setState(
            {
              showFontWrap: true,
            },
            () => {
              this.showFontInit()
            }
          )
        }
      }
    
      handleSelectFontFamily = font => {
        this.changeFontObjField(font, true, false)
        const {showFontWrap} = this.state
        if (!showFontWrap) {
          this.setState(
            {
              showFontWrap: true,
            },
            () => {
              this.showFontInit()
            }
          )
        }
        this.setState(
          {
            currentFontFamily: font.family,
          },
          () => {
            this.changeFontObjField(font, false, false)
          }
        )
      }
    
      changeFontObjField = (fontObj, loading, download) => {
        const {fontFamilySelector} = this.state
        const newFontFamilySelector = fontFamilySelector.map(item => {
          if (item.family === fontObj.family) {
            return {...item, showLoading: loading, showDownload: download}
          }
          return item
        })
        this.setState({fontFamilySelector: newFontFamilySelector})
      }
    
      renderPosterCoverTab = () => {
        const {newPosterList} = this.props
        const {img} = this.state
        return (
          <div className="PosterEditorTool__PosterCoverTab">
            <div
              className="PosterEditorTool__PosterCoverTab__posterUpload"
              onClick={this.uploadImg}
              role="button"
              tabIndex={0}
            />
            {newPosterList.map(item => (
              <div
                className={cx('PosterEditorTool__PosterCoverTab__posterItem', {
                  'PosterEditorTool__PosterCoverTab__posterItem--selected':
                    img.id === item.id,
                })}
                key={item.id}
                style={{
                  // backgroundImage: `url(${'https://file.izuiyou.com/img/png/id/1090013920'})`,
                  backgroundImage: `url(${getImgUrl({id: item.id})})`,
                }}
                onClick={() => {
                  this.setDefaultImg(item.id)
                }}
                role="button"
                tabIndex={0}
              />
            ))}
          </div>
        )
      }
    
      renderFontSelectWrap = () => {
        const {currentFontFamily, fontFamilySelector} = this.state
        return (
          <div className="PosterEditorTool__PosterTextTab__fontSelectWrap">
            {fontFamilySelector.map((font, index) => (
              <div
                className={cx('PosterEditorTool__PosterTextTab__fontSelectItem', {
                  'PosterEditorTool__PosterTextTab__fontSelectItem--selected':
                    currentFontFamily === font.family,
                  'PosterEditorTool__PosterTextTab__fontSelectItem--loading':
                    font.showLoading === true,
                })}
                key={font.family}
                role="button"
                tabIndex={0}
                onClick={() => {
                  this.handleSelectFontFamily(font)
                }}
              >
                <div
                  className={`PosterEditorTool__PosterTextTab__fontItemBg PosterEditorTool__PosterTextTab__fontItemBg--${index}`}
                />
                {font.showDownload && (
                  <div className="PosterEditorTool__PosterTextTab__fontDownloadIcon" />
                )}
                {font.showLoading && (
                  <div className="PosterEditorTool__PosterTextTab__fontDownloadLoading">
                    <AsyncLottie
                      options={lottieOpts}
                      animationDataLoader={() => lottie}
                      height="100%"
                      width="100%"
                    />
                  </div>
                )}
              </div>
            ))}
          </div>
        )
      }
    
      renderPosterTextTab = () => {
        const {currentFontColor} = this.state
        return (
          <div className="PosterEditorTool__PosterTextTab">
            <div className="PosterEditorTool__PosterTextTab__colorWrap">
              {COLOR_SELECTOR.map(color => (
                <div
                  className="PosterEditorTool__PosterTextTab__colorItemWrap"
                  key={color}
                  role="button"
                  tabIndex={0}
                  onClick={() => {
                    this.handleSelectColor(color)
                  }}
                >
                  {currentFontColor === color ? (
                    <div
                      className="PosterEditorTool__PosterTextTab__colorItemSelected"
                      style={{
                        border:
                          color === GREY_STROKE
                            ? '1px solid #EAEAEA'
                            : `2px solid ${color}`,
                      }}
                    >
                      <span
                        style={{
                          background: color,
                          border:
                            color === GREY_STROKE ? '1px solid #EAEAEA' : 'none',
                        }}
                      />
                    </div>
                  ) : (
                    <div
                      className="PosterEditorTool__PosterTextTab__colorItem"
                      style={{
                        background: color,
                        border:
                          STROKE_COLOR.indexOf(color) > -1
                            ? '1px solid #EAEAEA'
                            : 'none',
                      }}
                    />
                  )}
                </div>
              ))}
            </div>
            {this.renderFontSelectWrap()}
          </div>
        )
      }
    
      render() {
        const {
          currentTabValue,
          img,
          showFontWrap,
          showTextInputCom,
          fontWrapContent,
          currentFontColor,
          currentFontFamily,
          toCanvas,
        } = this.state
        return (
          <div className="PosterEditorTool">
            <div className="PosterEditorTool__header">
              <span onClick={this.handleClickComplete} role="button" tabIndex={0}>
                完成
              </span>
            </div>
            <div
              ref={ref => {
                this.canvasRef = ref
              }}
              className="PosterEditorTool__posterWrap"
            >
              <img
                src={getImgUrl(img) || bgUrl}
                alt=""
                className="PosterEditorTool__posterImg"
              />
              {showFontWrap && (
                <div
                  ref={ref => {
                    this.dragRef = ref
                  }}
                  className="PosterEditorTool__dragResizeContainer"
                  style={{
                    border: !toCanvas ? '1px solid #FFFFFF' : 'none',
                    top: '0px',
                    left: '0px',
                    fontSize: '24px',
                  }}
                  role="button"
                  tabIndex={0}
                  onClick={evt => {
                    this.setState({
                      currentTabValue: 1,
                      showTextInputCom: true,
                    })
                  }}
                >
                  <span
                    className="PosterEditorTool__posterTextWrap"
                    style={{
                      color: currentFontColor,
                      fontFamily: currentFontFamily,
                    }}
                  >
                    {fontWrapContent}
                  </span>
                  {!toCanvas && (
                    <div
                      className="PosterEditorTool__closeHandle"
                      role="button"
                      tabIndex={0}
                      onClick={evt => {
                        evt.stopPropagation()
                        this.handleCloseFont()
                      }}
                    />
                  )}
                  {!toCanvas && (
                    <div
                      ref={ref => {
                        this.resizeRef = ref
                      }}
                      className="PosterEditorTool__resizeHandle"
                      role="button"
                      tabIndex={0}
                    />
                  )}
                </div>
              )}
            </div>
            <div className="PosterEditorTool__bottomContainer">
              <div className="PosterEditorTool__tabPanel">
                {TAB_PANEL.items().map(item => (
                  <div
                    className={cx('PosterEditorTool__tabPanelItem', {
                      'PosterEditorTool__tabPanelItem--selected':
                        item.value === currentTabValue,
                    })}
                    key={item.value}
                    onClick={() => {
                      this.changeTab(item.value)
                    }}
                    role="button"
                    tabIndex={0}
                  >
                    <span>{item.text}</span>
                  </div>
                ))}
              </div>
              {currentTabValue === TAB_PANEL.alias2Value('posterCover') &&
                this.renderPosterCoverTab()}
              {currentTabValue === TAB_PANEL.alias2Value('posterText') &&
                this.renderPosterTextTab()}
            </div>
            {showTextInputCom && (
              <TextInput
                onConfirm={this.handleConfirm}
                currentFontColor={currentFontColor}
                currentFontFamily={currentFontFamily}
                fontWrapContent={fontWrapContent}
              />
            )}
          </div>
        )
      }
    }
    
    PosterEditorTool.propTypes = {
      onComplete: PT.func,
      newPosterList: PT.arrayOf(PT.shape()),
    }
    PosterEditorTool.defaultProps = {
      onComplete: () => {},
      newPosterList: [],
    }
    
    export default connect((state, props) => {
      const {
        topic: {activity = {}},
      } = state
      const {newPosterList} = activity
      return {newPosterList}
    })(PosterEditorTool)

    css如下:

    @font-face {
      font-family: "PangMenZhengDao";
      src: url("./fonts/PangMenZhengDao.ttf");
    }
    @font-face {
      font-family: "SourceHanSansCN-Bold";
      src: url("./fonts/SourceHanSansCN-Bold.otf");
    }
    @font-face {
      font-family: "zcool-gdh";
      src: url("./fonts/zcool-gdh.ttf");
    }
    @font-face {
      font-family: "ZCOOL_KuHei";
      src: url("./fonts/ZCOOL_KuHei.ttf");
    }
    @font-face {
      font-family: "SourceHanSerifCN-Heavy";
      src: url("./fonts/SourceHanSerifCN-Heavy.otf");
    }
    @font-face {
      font-family: "jiangxizhuokai-Regular";
      src: url("./fonts/jiangxizhuokai-Regular.ttf");
    }
    @font-face {
      font-family: "HappyZcool-2016";
      src: url("./fonts/HappyZcool-2016.ttf");
    }
    @font-face {
      font-family: "JiangChengYuanTi-500W";
      src: url("./fonts/JiangChengYuanTi-500W.ttf");
    }
    @font-face {
      font-family: "zcoolwenyiti";
      src: url("./fonts/zcoolwenyiti.ttf");
    }
    @font-face {
      font-family: "xiaowei";
      src: url("./fonts/xiaowei.otf");
    }
    @font-face {
      font-family: "HanaMinPlus";
      src: url("./fonts/HanaMinPlus.ttf");
    }
    .PosterEditorTool {
      position: fixed;
      width: 100%;
      height: 100%;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      background: #000000;
      box-sizing: border-box;
      z-index: 999;
      padding-top: 10px;
    
      &__header {
        font-family: PingFang SC;
        font-style: normal;
        font-weight: normal;
        font-size: 17px;
        line-height: 24px;
        text-align: right;
        color: #FFFFFF;
        padding-right: 10px;
      }
    
      &__posterWrap {
        position: relative;
        width: 100vw;
        height: 33.33333333vw;
        margin-top: 100px;
      }
      &__posterImg {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
      &__dragResizeContainer {
        position: absolute;
        box-sizing: border-box;
        padding: 13px 25px;
        font-size: 24px;
        background-color: transparent;
        white-space: nowrap;
        top: 0px;
        left: 0px;
      }
      &__posterTextWrap {
        color: #ffffff;
        white-space: nowrap;
      }
      &__resizeHandle {
        z-index: 11;
        width: 24px;
        height: 24px;
        position: absolute;
        background: url("./images/drag_icon.png") no-repeat center center / 100% 100% transparent;
        right: -10px;
        bottom: -10px;
      }
      &__closeHandle {
        z-index: 11;
        width: 24px;
        height: 24px;
        position: absolute;
        background: url("./images/font_close_icon.png") no-repeat center center / 100% 100% transparent;
        right: -10px;
        top: -10px;
      }
      &__bottomContainer {
        position: absolute;
        width: 100%;
        height: 320px;
        left: 0px;
        bottom: 0px;
        background: #FFFFFF;
        display: flex;
        flex-direction: column;
      }
      &__tabPanel {
        display: flex;
        padding: 11px 0 0 16px;
        flex-shrink: 0;
      }
      &__tabPanelItem {
        display: flex;
        flex-direction: column;
        align-items: center;
        &:nth-of-type(2) {
          margin-left: 26px;
        }
        span {
          font-family: PingFang SC;
          font-style: normal;
          font-weight: normal;
          font-size: 16px;
          line-height: 22px;
          color: #626470;
        }
        &--selected {
          span {
            font-weight: 500;
            color: #242529;
          }
          &::after {
            margin-top: 2px;
            display: inline-block;
            content: '';
            width: 9px;
            height: 3px;
            background: url("./images/panel_line_icon.png") no-repeat center center / 100% 100% transparent;
          }
        }
      }
      &__PosterCoverTab {
        flex-grow: 1;
        overflow-y: scroll;
        padding: 12px 16px 0 16px;
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        &__posterItem, &__posterUpload {
          width: 44.8vw;
          height: 14.933333vw;
          border-radius: 4px;
          background-repeat: no-repeat;
          background-size: 100% auto;
          flex-shrink: 0;
        }
        &__posterItem {
          margin-top: 7px;
          box-sizing: border-box;
          &--selected {
            border: 1.5px solid #149EFF;
          }
          &:nth-of-type(2) {
            margin-top: 0px;
          }
        }
        &__posterUpload {
          background-image: url('./images/upload_bg_icon.png');
        }
      }
      &__PosterTextTab {
        flex-grow: 1;
        overflow-y: scroll;
        padding-top: 12px;
    
        &__fontSelectWrap {
          padding: 10px 16px 16px 16px;
          padding-bottom: calc(16px + env(safe-area-inset-bottom));
          display: flex;
          flex-wrap: wrap;
          justify-content: space-between;
        }
        &__fontSelectItem {
          width: 81px;
          height: 56px;
          background: #F5F5F7;
          border-radius: 6px;
          box-sizing: border-box;
          margin-top: 6px;
          position: relative;
          &--selected {
            background: rgba(20, 158, 255, 0.1);
            border: 1.5px solid #149EFF;
          }
          &--loading {
            background: rgba(0, 0, 0, 0.1);
            border: none;
            pointer-events: none;
          }
        }
        &__fontItemBg {
          width: 100%;
          height: 100%;
          &--0 {
            display: flex;
            justify-content: center;
            align-items: center;
            &::after {
              content: '默认字体';
              font-family: '';
              font-style: normal;
              font-weight: normal;
              font-size: 13px;
              color: #626470;
            }
          }
          &--1 {
            background: url("./images/font_icon_1.png") no-repeat center center / 100% 100% transparent;
          }
          &--2 {
            background: url("./images/font_icon_2.png") no-repeat center center / 100% 100% transparent;
          }
          &--3 {
            background: url("./images/font_icon_3.png") no-repeat center center / 100% 100% transparent;
          }
          &--4 {
            background: url("./images/font_icon_4.png") no-repeat center center / 100% 100% transparent;
          }
          &--5 {
            background: url("./images/font_icon_5.png") no-repeat center center / 100% 100% transparent;
          }
          &--6 {
            background: url("./images/font_icon_6.png") no-repeat center center / 100% 100% transparent;
          }
          &--7 {
            background: url("./images/font_icon_7.png") no-repeat center center / 100% 100% transparent;
          }
          &--8 {
            background: url("./images/font_icon_8.png") no-repeat center center / 100% 100% transparent;
          }
          &--9 {
            background: url("./images/font_icon_9.png") no-repeat center center / 100% 100% transparent;
          }
          &--10 {
            background: url("./images/font_icon_10.png") no-repeat center center / 100% 100% transparent;
          }
          &--11 {
            background: url("./images/font_icon_11.png") no-repeat center center / 100% 100% transparent;
          }
        }
        &__fontDownloadIcon {
          position: absolute;
          width: 16px;
          height: 16px;
          background: url("./images/font_download_icon.png") no-repeat center center / 100% 100% transparent;
          right: -2px;
          bottom: -2px;
        }
        &__fontDownloadLoading {
          position: absolute;
          width: 20px;
          height: 20px;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
        }
        &__colorWrap {
          overflow-x: scroll;
          padding-left: 16px;
          display: flex;
          align-items: center;
        }
        &__colorItemWrap {
          margin-left: 12px;
          &:nth-of-type(1) {
            margin-left: 0px;
          }
        }
        &__colorItem {
          width: 24px;
          height: 24px;
          border-radius: 50%;
          box-sizing: border-box;
        }
        &__colorItemSelected {
          width: 24px;
          height: 24px;
          box-sizing: border-box;
          background-color: #ffffff;
          border-radius: 50%;
          display: flex;
          align-items: center;
          justify-content: center;
          span {
            display: inline-block;
            width: 12px;
            height: 12px;
            border-radius: 50%;
          }
        }
      }
    }

    注意点:在使用html2canvas时,如果HTML内包含需要请求接口URL的图片时,会有跨域问题,需要URL域名设置允许跨域。

  • 相关阅读:
    biji001
    公司内部openStack环境信息
    def
    CI调试
    一:Java之面向对象基本概念
    STL_算法_Heap算法(堆排)(精)
    IOS开发-经常使用站点集合
    【iOS开发-47】怎样下载iOS 7.1 Simulator 以及iOS 8离线的Documentation这些文件?
    设计模式简介
    how to deal with &quot;no such file error or diretory&quot; error for a new programmer in QT creator
  • 原文地址:https://www.cnblogs.com/chenbeibei520/p/14313190.html
Copyright © 2011-2022 走看看