拖动缩放功能

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

需要用到的插件: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域名设置允许跨域。

posted @ 2021-01-22 14:38  贝子涵夕  阅读(168)  评论(0编辑  收藏  举报