Skip to content

Commit

Permalink
feat: mask editor
Browse files Browse the repository at this point in the history
  • Loading branch information
surunzi committed Feb 23, 2024
1 parent 54d16f4 commit 0295a76
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 56 deletions.
48 changes: 48 additions & 0 deletions src/mask-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Luna Mask Editor

Image mask editing.

## Demo

https://luna.liriliri.io/?path=/story/mask-editor

## Install

Add the following script and style to your page.

```html
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/luna-toolbar/luna-toolbar.css" />
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/luna-painter/luna-painter.css" />
<script src="//cdn.jsdelivr.net/npm/luna-toolbar/luna-toolbar.js"></script>
<script src="//cdn.jsdelivr.net/npm/luna-painter/luna-painter.js"></script>
<script src="//cdn.jsdelivr.net/npm/luna-mask-editor/luna-mask-editor.js"></script>
```

You can also get it on npm.

```bash
npm install luna-mask-editor luna-painter luna-toolbar --save
```

```javascript
import 'luna-toolbar/luna-toolbar.css'
import 'luna-painter/luna-painter.css'
import LunaMaskEditor from 'luna-mask-editor'
```

## Usage

```javascript
const container = document.getElementById('container')
const maskEditor = new LunaMaskEditor(container)
```

## Configuration

* image(string): Image src.

## Api

### getCanvas(): HTMLCanvasElement

Get a canvas with mask drawn.
108 changes: 85 additions & 23 deletions src/mask-editor/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Component, { IComponentOptions } from '../share/Component'
import LunaPainter from 'luna-painter'
import LunaPainter, { Layer, Zoom } from 'luna-painter'
import debounce from 'licia/debounce'
import Color from 'licia/Color'

Expand All @@ -19,8 +19,15 @@ export interface IOptions extends IComponentOptions {
export default class MaskEditor extends Component<IOptions> {
private painter: LunaPainter
private canvas: HTMLCanvasElement
private blackCanvas: HTMLCanvasElement
private blackCtx: CanvasRenderingContext2D
private ctx: CanvasRenderingContext2D
private maskBrush: MaskBrush
private baseLayer: Layer
private drawingLayer: Layer
constructor(container: HTMLElement, options: IOptions) {
super(container, { compName: 'mask-editor' }, options)
this.initOptions(options)

this.initTpl()

Expand All @@ -30,47 +37,82 @@ export default class MaskEditor extends Component<IOptions> {
tools: [],
}
)
painter.addTool('paintBucket', new MaskPaintBucket(painter))
painter.addTool('eraser', new MaskEraser(painter))
const maskBrush = new MaskBrush(painter)
painter.addTool('brush', maskBrush)
this.maskBrush = new MaskBrush(painter)
painter.addTool('brush', this.maskBrush)
painter.useTool('brush')
this.painter = painter
this.addSubComponent(this.painter)

const image = new Image()
image.onload = function () {
const ctx = painter.getActiveLayer().getContext()
ctx.drawImage(image, 0, 0, image.width, image.height)
const idx = painter.addLayer()
painter.activateLayer(idx)
const layer = painter.getActiveLayer()
layer.opacity = 80
maskBrush.on('optionChange', (name, val) => {
if (name === 'layerOpacity') {
layer.opacity = val
painter.renderCanvas()
}
})
painter.renderCanvas()
}
image.src = options.image
this.baseLayer = painter.getActiveLayer()
const idx = painter.addLayer()
painter.activateLayer(idx)
this.drawingLayer = painter.getActiveLayer()
this.drawingLayer.opacity = 80
painter.renderCanvas()

this.addSubComponent(this.painter)
this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')!
this.blackCanvas = document.createElement('canvas')
this.blackCtx = this.blackCanvas.getContext('2d')!

this.bindEvent()

this.loadImage()
}
/** Get a canvas with mask drawn. */
getCanvas() {
return this.canvas
}
private loadImage() {
const { painter } = this

const image = new Image()
image.onload = () => {
const { width, height } = image
painter.setOption({
width,
height,
})

const ctx = this.baseLayer.getContext()
ctx.drawImage(image, 0, 0, width, height)
painter.renderCanvas()
const zoom = painter.getTool('zoom') as Zoom
zoom.fitScreen()

this.renderMask()
}
image.src = this.options.image
}
private renderMask() {
const { canvas, ctx, blackCanvas, blackCtx, painter } = this
const { width, height } = painter.getCanvas()

blackCanvas.width = width
blackCanvas.height = height
blackCtx.fillStyle = '#000000'
blackCtx.fillRect(0, 0, width, height)
blackCtx.globalCompositeOperation = 'destination-in'
blackCtx.drawImage(painter.getActiveLayer().getCanvas(), 0, 0)

canvas.width = width
canvas.height = height
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, width, height)
ctx.drawImage(blackCanvas, 0, 0)
}
private initTpl() {
this.$container.html(this.c('<div class="painter"></painter>'))
}
private bindEvent() {
const { painter } = this
const { painter, maskBrush } = this

painter.on(
'canvasRender',
debounce(() => {
this.renderMask()
this.emit('change')
}, 20)
)
Expand All @@ -79,7 +121,7 @@ export default class MaskEditor extends Component<IOptions> {
const c = new Color(color)
const rgb = Color.parse(c.toRgb()).val

const ctx = painter.getActiveLayer().getContext()
const ctx = this.drawingLayer.getContext()
const canvas = ctx.canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const { data } = imageData
Expand All @@ -91,6 +133,19 @@ export default class MaskEditor extends Component<IOptions> {
ctx.putImageData(imageData, 0, 0)
painter.renderCanvas()
})

maskBrush.on('optionChange', (name, val) => {
if (name === 'layerOpacity') {
this.drawingLayer.opacity = val
painter.renderCanvas()
}
})

this.on('optionChange', (name) => {
if (name === 'image') {
this.loadImage()
}
})
}
}

Expand Down Expand Up @@ -145,3 +200,10 @@ class MaskEraser extends LunaPainter.Eraser {
})
}
}

class MaskPaintBucket extends LunaPainter.PaintBucket {
constructor(painter: LunaPainter) {
super(painter)
this.options.tolerance = 180
}
}
3 changes: 2 additions & 1 deletion src/mask-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"toolbar",
"painter"
],
"style": false
"style": false,
"react": true
}
}
33 changes: 33 additions & 0 deletions src/mask-editor/react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CSSProperties, FC, useEffect, useRef } from 'react'
import MaskEditor, { IOptions } from './index'
import { useNonInitialEffect } from '../share/hooks'

interface IMaskEditorProps extends IOptions {
style?: CSSProperties
onCreate?: (maskEditor: MaskEditor) => void
}

const LunaMaskEditor: FC<IMaskEditorProps> = (props) => {
const maskEditorRef = useRef<HTMLDivElement>(null)
const maskEditor = useRef<MaskEditor>()

useEffect(() => {
const { image } = props
maskEditor.current = new MaskEditor(maskEditorRef.current!, {
image,
})
props.onCreate && props.onCreate(maskEditor.current)

return () => maskEditor.current?.destroy()
}, [])

useNonInitialEffect(() => {
if (maskEditor.current) {
maskEditor.current.setOption('image', props.image)
}
}, [props.image])

return <div ref={maskEditorRef} style={props.style}></div>
}

export default LunaMaskEditor
102 changes: 89 additions & 13 deletions src/mask-editor/story.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,98 @@
import MaskEditor from 'luna-mask-editor.js'
import story from '../share/story'
import $ from 'licia/$'
import h from 'licia/h'
import readme from './README.md'
import LunaMaskEditor from './react'
import { text } from '@storybook/addon-knobs'
import { useRef } from 'react'

const def = story('mask-editor', (container) => {
$(container).css({
width: '100%',
maxWidth: 1200,
height: 600,
margin: '0 auto',
})
const def = story(
'mask-editor',
(wrapper) => {
$(wrapper).html('')

const maskEditor = new MaskEditor(container, {
image: 'https://res.liriliri.io/luna/pic1.jpg',
})
const container = h('div')

$(container).css({
width: '100%',
maxWidth: 1200,
height: 600,
margin: '0 auto',
})
wrapper.appendChild(container)

const maskContainer = h('div')
$(maskContainer).css({
border: '1px solid #eee',
width: '100%',
maxWidth: 1200,
margin: '0 auto',
fontSize: 0,
marginTop: '10px',
})
wrapper.appendChild(maskContainer)

const { image } = createKnobs()
const maskEditor = new MaskEditor(container, {
image,
})

onCreate(maskEditor, maskContainer)

return maskEditor
})
return maskEditor
},
{
readme,
source: __STORY__,
ReactComponent({ theme }) {
const maskContainer = useRef(null)
const { image } = createKnobs()

return (
<>
<LunaMaskEditor
theme={theme}
image={image}
onCreate={(maskEditor) => {
if (maskContainer.current) {
onCreate(maskEditor, maskContainer.current)
}
}}
/>
<div
style={{
border: '1px solid #eee',
width: '100%',
maxWidth: 1200,
margin: '0 auto',
fontSize: 0,
marginTop: '10px',
}}
ref={maskContainer}
></div>
</>
)
},
}
)

function createKnobs() {
const image = text('Image', 'https://res.liriliri.io/luna/pic1.jpg')

return {
image,
}
}

function onCreate(maskEditor, maskContainer) {
const canvas = maskEditor.getCanvas()
maskContainer.appendChild(canvas)
$(canvas).css({
maxWidth: '100%',
})
}

export default def

export const { maskEditor } = def
export const { maskEditor: html, react } = def
Loading

0 comments on commit 0295a76

Please sign in to comment.