DEV Community

Cover image for Develop mobile H5 image editor details using fabric.js
秦少卫
秦少卫

Posted on

Develop mobile H5 image editor details using fabric.js

Hello everyone! I am the author of the open-source image editor at https://github.com/ikuaitu/vue-fabric-editor. It is an open-source image editor based on the PC version.

Recently, many developers have asked whether the open-source image editor can be transformed into an H5 version image editor suitable for mobile terminals. Recently, as soon as the H5 version of the image editor was launched, I organized the implementation ideas and product details into notes and shared them for your reference.

Preview

Foundation

The basic functions of the open source image editor are available, such as switching templates, adding elements, custom fonts, etc., but compared to the interaction on the mobile side, there will be great differences, and a lot of modifications have been made. This note mainly shares the idea and details of the mobile image editor .

Preview1

Preview2

Preview3

Outline

  1. Switch templates.
  2. Add images.
  3. Add composite elements.
  4. Set background color.
  5. Modify canvas size.
  6. Quick menu.
  7. Property toolbar.
  8. Special effect fonts.
  9. Switch fonts.
  10. Input text.
  11. Text layout.
  12. Border.
  13. Shadow.
  14. Download images.

Note: Some code examples are packaged code and are not native to fabric.js.

1. Switching template

Switching template

The editor is developed based on fabric.js, and all templates are stored in json format. To switch templates, you only need to request the details interface and add the data in json format to the canvas. It is necessary to pay attention to the font name used in the template and load the font file before rendering. Otherwise the font style won't render properly.

const loadInfo = async (res: any) => {
  const info = res.data
  templName.value = info.name;
  await canvasEditor.getFontList(JSON.stringify(info.json));
  canvasEditor.loadJSON(JSON.stringify(info.json), () => LoadingPlugin(false));
};

Enter fullscreen mode Exit fullscreen mode

2. Add picture

Add picture

There are many ways to add pictures in fabric.js, we can use the simplest fabric.Image.fromURL, in addition, there are often pictures larger than the canvas, but also need to scale the picture according to the width of the canvas, more convenient for users to operate.

const toEditor = async (e: MouseEvent) => {
  visible.value = false
  LoadingPlugin(true)
  const item = await canvasEditor.createImgByElement(e.target as HTMLImageElement)
  await canvasEditor.addBaseType(item, { scale: true })
  LoadingPlugin(false)
}
Enter fullscreen mode Exit fullscreen mode

3. Add combination element

Add combination element

Fabric.js supports exporting/importing individual elements in JSON format. If we store the exported data in the database, we can import it according to the element type when importing. We need to obtain the type of the element in JSON and call it as a method name. We also need to do font loading before importing and scaling after pouring.

const capitalizeFirstLetter = (str: string) => {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

const toEditor = async (item: ItemProps) => {
  visible.value = false
  LoadingPlugin(true)
  await canvasEditor.downFontByJSON(JSON.stringify(item.json));
  const el = JSON.parse(JSON.stringify(item.json));
  const elType = capitalizeFirstLetter(el.type);
  new fabric[elType].fromObject(el, (fabricEl: fabric.Object) => {
    canvasEditor.dragAddItem(fabricEl);
    LoadingPlugin(false)
  });
}
Enter fullscreen mode Exit fullscreen mode

4. Set background color

Set background color

Setting the background color is relatively simple. You can set the color according to the API of fabrics. It should be noted that most of the color components on the PC side do not adapt to the scene of mobile end H5, and do not support touch events. We used the "@jaames/iro" component, which performs well on mobile end and fully adapts to our scene. Its API is very flexible. We encapsulated it into a general color component and called it in multiple places.

<template>
  <div ref="pickerContainer">
  </div>
</template>

<script setup lang="ts">
import iro from '@jaames/iro';
const emit = defineEmits(['update:modelValue', 'change']);

const props = defineProps({
  modelValue: {
    type: String,
    default: '#000000'
  },
  width: {
    type: Number,
    default: 200
  }
});

const pickerContainer = ref<HTMLButtonElement | string>('');
let colorPicker: any = null;

onMounted(() => {
  // 创建iro.js颜色选择器
  colorPicker = iro.ColorPicker(pickerContainer.value, {
    width: props.width,
    color: props.modelValue,
    borderWidth: 1,
    borderColor: "#fff",
    layoutDirection: 'horizontal',
    layout: [
      {
        component: iro.ui.Slider,
        options: {
          id: 'hue-slider',
          sliderType: 'hue'
        }
      },
      {
        component: iro.ui.Box,
      },
      {
        component: iro.ui.Slider,
        options: {
          sliderType: 'alpha'
        }
      }
    ]
  });

  // 监听颜色变化事件并发射自定义事件
  colorPicker.on('color:change', (color: any) => {
    const rgbaString = color.rgbaString;
    emit('update:modelValue', rgbaString);
    emit('change', rgbaString);
  });
});

</script>
Enter fullscreen mode Exit fullscreen mode

5. Modify canvas size

Modify canvas size


const resizeEditor = async () => {
    await nextTick()
    const editorWorkspase = document.querySelector('#workspace') as HTMLElement
    const popElement = document.querySelector('.my-editor-popup') as HTMLElement
    const headerElement = document.querySelector('.t-navbar') as HTMLElement
    if (popElement) {
      editorWorkspase.style.height = `calc(100vh - ${popElement?.offsetHeight + headerElement?.offsetHeight || 0}px)`
    } else {
      editorWorkspase.style.height = ''
    }
  }

Enter fullscreen mode Exit fullscreen mode

6. Shortcut menu

Shortcut menu

Many shortcut operations need to be able to allow users to quickly find and complete the operation. We have added a shortcut menu function to the element to avoid allowing users to click on the bottom menu bar. When the element is selected, it will be automatically displayed and hidden when unchecked. It should be noted that the shortcut menu is not always above the element. The shortcut menu should be positioned according to the element position and the size of the canvas. When the menu exceeds the canvas area, we need to adjust the menu position in time. In addition, when the attribute pop-up box appears and the canvas size changes, the menu position needs to be modified synchronously.

// 更新位置信息
const upDatePosition = async () => {
  const activeObject = canvasEditor.canvas.getActiveObject();
  if (activeObject) {
    canvasEditor.canvas.renderAll();
    fixLeft.value = 10;
    fixTop.value = 10;
    await nextTick();
    isIncluded(activeObject);
    await nextTick();
  }
}

// 监听选中对象变化更新位置信息
getObjectAttr(upDatePosition)
canvasEditor.canvas.on('selection:updated', upDatePosition)
canvasEditor.canvas.on('mouse:move', upDatePosition)
canvasEditor.on('workspaceAutoEvent', upDatePosition)
Enter fullscreen mode Exit fullscreen mode

7. Properties toolbar

Properties toolbar

Referring to other image editors, some properties will have modifiable options only after an element is clicked. When deselected, the options will be hidden. In addition, different selected elements will have different modifiable options. This is a very excellent interaction in making a complex image editor on mobile terminals.

We have encapsulated common selection types and methods, and set hiding/showing separately for each property component.

Image description

8. Special effect fonts

Special effect fonts

Special effect fonts are mainly a combination of the color, border, and shadow of text elements. We will export the JSON after setting the style of the text and save it in the database. When a certain special effect is selected, the attributes can be set to the element according to the data in the JSON.

const setStyle = (item: ImgItem) => {
  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
  if (activeObject) {
    const values = toRaw(item.json);
    const keys = ['fill', 'stroke', 'strokeWidth', 'shadow', 'strokeLineCap'];
    activeObject.set('paintFirst', 'stroke');
    keys.forEach((key) => {
      activeObject.set(key, values[key]);
      if (key === 'fill' && typeof values[key] != 'string') {
        activeObject.set(key, new fabric.Gradient(values[key]));
      }
    });
    canvasEditor.canvas.renderAll();
  }
};
Enter fullscreen mode Exit fullscreen mode

9. Switch font

Image description

To modify the font, you only need to call the'fontFamily 'property of the structure.js element. Make sure the font is loaded before modifying it.


const changeCommon = async (key: string, value: any) => {
  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
  if (activeObject) {
    LoadingPlugin(true);
    baseAttr.fontFamily = value;
    try {
      await canvasEditor.loadFont(value)
    } catch (error) {
      console.log(error)
    }
    LoadingPlugin(false);
    activeObject && activeObject.set(key, value);
    canvasEditor.canvas.renderAll();
  }
};
Enter fullscreen mode Exit fullscreen mode

10. Enter text

Enter text

Fabric.js can be modified by double-clicking the text element directly, but this interaction is not noticeable in the mobile end. We have modified the text element separately. After selecting the element, the text box will pop up when clicking again. You can click the button in the bottom menu bar to modify it.

11. Text layout

Text layout

Text typesetting is relatively simple. We only need to set the text properties according to the text properties of fabrics, such as fontSize, lineHeight, charSpacing, etc.

// 属性值
const baseAttr = reactive({

  fontSize: 0,
  lineHeight: 0,
  charSpacing: 0,
  textAlign: '',

  fontWeight: '',
  fontStyle: '',

  underline: false,
  linethrough: false,
  overline: false,
});


Enter fullscreen mode Exit fullscreen mode

12. Border

Border

The border style is similar to the text style, and it can be quickly implemented with the color component.

// 属性值
const baseAttr = reactive({
  stroke: '#fff',
  strokeWidth: 0,
  strokeDashArray: [],
});

Enter fullscreen mode Exit fullscreen mode

13. Shadow

Shadow

The reference attribute is mainly a modification of the shadow sub-attribute of the element. The code is as follows:

// 属性值
const baseAttr = reactive({
  shadow: {
    color: '#fff',
    blur: 0,
    offsetX: 1,
    offsetY: 1,
  }
});


// 通用属性改变
const changeCommon = () => {
  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
  if (activeObject) {
    activeObject.set('shadow', new fabric.Shadow(baseAttr.shadow));
    canvasEditor.canvas.renderAll();
  }
};
Enter fullscreen mode Exit fullscreen mode

14. Download image

fabric.js can export images in Png/Jpeg/Base64 formats. At the same time, for the JPEG format, the image quality and size multiple can also be specified. For details, please refer to the API documentation of fabric.js.

End

The above are the implementation details of developing a mobile editor with fabric.js. Combined with our open-source project and plug-in architecture, project development can be completed conveniently. If you are working on a similar project or doing a similar project, welcome to communicate with me.

Open source project:https://github.com/ikuaitu/vue-fabric-editor/blob/main/README-zh.md

Top comments (0)