bak
This commit is contained in:
1 parent
43933f93d1
commit
03b1147a93
10 files changed
+804
-80
No files matched your search
+134
-1
@@ -33,6 +33,7 @@ interface MountedHandle {
|
||||
interface RecordingCtx {
|
||||
ctx: CanvasRenderingContext2D
|
||||
filterSets: string[]
|
||||
compositeOps: string[]
|
||||
}
|
||||
|
||||
function makeRecordingCtx(): RecordingCtx {
|
||||
@@ -41,9 +42,11 @@ function makeRecordingCtx(): RecordingCtx {
|
||||
canvas.height = 200
|
||||
const real = canvas.getContext('2d')!
|
||||
const filterSets: string[] = []
|
||||
const compositeOps: string[] = []
|
||||
const proxy = new Proxy(real, {
|
||||
set(target, prop, value) {
|
||||
if (prop === 'filter') filterSets.push(value as string)
|
||||
if (prop === 'globalCompositeOperation') compositeOps.push(value as string)
|
||||
;(target as unknown as Record<string | symbol, unknown>)[prop as string | symbol] = value
|
||||
return true
|
||||
},
|
||||
@@ -55,7 +58,7 @@ function makeRecordingCtx(): RecordingCtx {
|
||||
return v
|
||||
},
|
||||
})
|
||||
return { ctx: proxy as CanvasRenderingContext2D, filterSets }
|
||||
return { ctx: proxy as CanvasRenderingContext2D, filterSets, compositeOps }
|
||||
}
|
||||
|
||||
function mountComponent(
|
||||
@@ -1571,6 +1574,136 @@ export function registerAllTests(): void {
|
||||
})
|
||||
})
|
||||
|
||||
suite('mask layers — store', () => {
|
||||
test('setLayerMask(true) enables positive mask by default', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'M', width: 40, height: 40, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)
|
||||
s.setLayerMask(l.id, true)
|
||||
const layer = s.scene.layers.find((x) => x.id === l.id)!
|
||||
expect(layer.isMask).toBe(true)
|
||||
expect(layer.maskMode).toBe('positive')
|
||||
})
|
||||
|
||||
test('setLayerMask(true, "negative") stores negative mode', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'circle', name: 'N', radius: 20, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)
|
||||
s.setLayerMask(l.id, true, 'negative')
|
||||
const layer = s.scene.layers.find((x) => x.id === l.id)!
|
||||
expect(layer.maskMode).toBe('negative')
|
||||
})
|
||||
|
||||
test('setLayerMask(false) clears mask flags', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'M', width: 40, height: 40, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)
|
||||
s.setLayerMask(l.id, true, 'negative')
|
||||
s.setLayerMask(l.id, false)
|
||||
const layer = s.scene.layers.find((x) => x.id === l.id)!
|
||||
expect(layer.isMask).toBe(undefined)
|
||||
expect(layer.maskMode).toBe(undefined)
|
||||
})
|
||||
|
||||
test('setLayerMaskMode flips mode without changing isMask', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'M', width: 40, height: 40, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)
|
||||
s.setLayerMask(l.id, true)
|
||||
s.setLayerMaskMode(l.id, 'negative')
|
||||
const layer = s.scene.layers.find((x) => x.id === l.id)!
|
||||
expect(layer.isMask).toBe(true)
|
||||
expect(layer.maskMode).toBe('negative')
|
||||
})
|
||||
})
|
||||
|
||||
suite('mask layers — rendering', () => {
|
||||
function setupMaskScene(mode: 'positive' | 'negative') {
|
||||
const s = makeFreshStore()
|
||||
s.scene.canvas.width = 100
|
||||
s.scene.canvas.height = 100
|
||||
// Content: full-canvas red rect
|
||||
const content = s.addAsset({
|
||||
kind: 'rect',
|
||||
name: 'BG',
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#ff0000',
|
||||
})
|
||||
const contentLayer = s.addLayer(content.id)
|
||||
s.upsertKeyframe(contentLayer.id, 0, { x: 50, y: 50 })
|
||||
// Mask: small circle centered at (50,50)
|
||||
const mask = s.addAsset({
|
||||
kind: 'circle',
|
||||
name: 'M',
|
||||
radius: 20,
|
||||
fill: '#ffffff',
|
||||
})
|
||||
const maskLayer = s.addLayer(mask.id)
|
||||
s.upsertKeyframe(maskLayer.id, 0, { x: 50, y: 50 })
|
||||
s.setLayerMask(maskLayer.id, true, mode)
|
||||
const group = s.addTrackGroup('G')
|
||||
s.moveTrackIntoGroup(maskLayer.id, group.id)
|
||||
s.moveTrackIntoGroup(contentLayer.id, group.id)
|
||||
return { store: s }
|
||||
}
|
||||
|
||||
test('positive mask keeps content inside the mask shape and clears outside', () => {
|
||||
const { store } = setupMaskScene('positive')
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 100
|
||||
canvas.height = 100
|
||||
const ctx = canvas.getContext('2d')!
|
||||
drawSceneFrame(ctx, store.scene, 0, {})
|
||||
const inside = ctx.getImageData(50, 50, 1, 1).data
|
||||
const outside = ctx.getImageData(5, 5, 1, 1).data
|
||||
// Inside mask: red content visible
|
||||
expect(inside[0] > 200).toBe(true)
|
||||
expect(inside[3] > 200).toBe(true)
|
||||
// Outside mask: cleared (alpha 0)
|
||||
expect(outside[3]).toBe(0)
|
||||
})
|
||||
|
||||
test('negative mask punches a hole in the content where the mask overlaps', () => {
|
||||
const { store } = setupMaskScene('negative')
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 100
|
||||
canvas.height = 100
|
||||
const ctx = canvas.getContext('2d')!
|
||||
drawSceneFrame(ctx, store.scene, 0, {})
|
||||
const inside = ctx.getImageData(50, 50, 1, 1).data
|
||||
const outside = ctx.getImageData(5, 5, 1, 1).data
|
||||
// Inside mask: hole (alpha 0)
|
||||
expect(inside[3]).toBe(0)
|
||||
// Outside mask: red content still there
|
||||
expect(outside[0] > 200).toBe(true)
|
||||
expect(outside[3] > 200).toBe(true)
|
||||
})
|
||||
|
||||
test('layer outside a masked group is not affected by the mask', () => {
|
||||
const { store } = setupMaskScene('positive')
|
||||
// Add a green rect layer OUTSIDE the group
|
||||
const outer = store.addAsset({
|
||||
kind: 'rect',
|
||||
name: 'Outer',
|
||||
width: 20,
|
||||
height: 20,
|
||||
fill: '#00ff00',
|
||||
})
|
||||
const outerLayer = store.addLayer(outer.id)
|
||||
store.upsertKeyframe(outerLayer.id, 0, { x: 90, y: 90 })
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 100
|
||||
canvas.height = 100
|
||||
const ctx = canvas.getContext('2d')!
|
||||
drawSceneFrame(ctx, store.scene, 0, {})
|
||||
// The outer layer renders fully regardless of mask
|
||||
const outerPx = ctx.getImageData(90, 90, 1, 1).data
|
||||
expect(outerPx[1] > 200).toBe(true)
|
||||
expect(outerPx[3] > 200).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
suite('scene store — shiftKeyframesPosition', () => {
|
||||
test('shifts x/y of a single keyframe by delta', async () => {
|
||||
const s = makeFreshStore()
|
||||
|
||||
Reference in new issue
Block a user