forked from theatre-js/theatre
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create idb-backed image prop (theatre-js#366)
Co-authored-by: Clement Roche <[email protected]>
- Loading branch information
1 parent
95b329b
commit 8d8e234
Showing
21 changed files
with
838 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/** | ||
* A super basic Turtle geometry renderer hooked up to Theatre, so the parameters | ||
* can be tweaked and animated. | ||
*/ | ||
import {getProject, types} from '@theatre/core' | ||
import studio from '@theatre/studio' | ||
import React, {useEffect, useState} from 'react' | ||
import {render} from 'react-dom' | ||
import styled from 'styled-components' | ||
|
||
studio.initialize() | ||
const project = getProject('Image type playground', { | ||
assets: { | ||
baseUrl: 'http://localhost:3000', | ||
}, | ||
}) | ||
const sheet = project.sheet('Image type') | ||
|
||
const Wrapper = styled.div` | ||
position: absolute; | ||
inset: 0; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
` | ||
|
||
const ImageTypeExample: React.FC<{}> = (props) => { | ||
const [imageUrl, setImageUrl] = useState<string>() | ||
|
||
useEffect(() => { | ||
const object = sheet.object('image', { | ||
image: types.image('', { | ||
label: 'texture', | ||
}), | ||
image2: types.image('', { | ||
label: 'another texture', | ||
}), | ||
something: 'asdf', | ||
}) | ||
object.onValuesChange(({image}) => { | ||
setImageUrl(project.getAssetUrl(image)) | ||
}) | ||
|
||
return () => { | ||
sheet.detachObject('canvas') | ||
} | ||
}, []) | ||
|
||
return ( | ||
<Wrapper | ||
onClick={() => { | ||
if (sheet.sequence.position === 0) { | ||
sheet.sequence.position = 0 | ||
sheet.sequence.play() | ||
} else { | ||
sheet.sequence.position = 0 | ||
} | ||
}} | ||
> | ||
<img src={imageUrl} /> | ||
</Wrapper> | ||
) | ||
} | ||
|
||
project.ready.then(() => { | ||
render(<ImageTypeExample />, document.getElementById('root')) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import {createStore} from './IDBStorage' | ||
import type Project from './Project' | ||
import type {IAssetStorageConfig} from './Project' | ||
// @ts-ignore | ||
import blobCompare from 'blob-compare' | ||
import {notify} from '@theatre/core/coreExports' | ||
import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets' | ||
|
||
export const createDefaultAssetStorageConfig = ({ | ||
project, | ||
baseUrl = '', | ||
}: { | ||
project: Project | ||
baseUrl?: string | ||
}): IAssetStorageConfig => { | ||
return { | ||
coreAssetStorage: { | ||
getAssetUrl: (assetId: string) => `${baseUrl}/${assetId}`, | ||
}, | ||
createStudioAssetStorage: async () => { | ||
// in SSR we bail out and return a dummy asset manager | ||
if (typeof window === 'undefined') { | ||
return { | ||
getAssetUrl: () => '', | ||
createAsset: () => Promise.resolve(null), | ||
} | ||
} | ||
|
||
// Check for support. | ||
if (!('indexedDB' in window)) { | ||
console.log("This browser doesn't support IndexedDB.") | ||
|
||
return { | ||
getAssetUrl: (assetId: string) => { | ||
throw new Error( | ||
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`, | ||
) | ||
}, | ||
createAsset: (asset: Blob) => { | ||
throw new Error( | ||
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`, | ||
) | ||
}, | ||
} | ||
} | ||
|
||
const idb = createStore(`${project.address.projectId}-assets`) | ||
|
||
// get all possible asset ids referenced by either static props or keyframes | ||
const possibleAssetIDs = getAllPossibleAssetIDs(project) | ||
|
||
// Clean up assets not referenced by the project. We can only do this at the start because otherwise | ||
// we'd break undo/redo. | ||
const idbKeys = await idb.keys<string>() | ||
await Promise.all( | ||
idbKeys.map(async (key) => { | ||
if (!possibleAssetIDs.includes(key)) { | ||
await idb.del(key) | ||
} | ||
}), | ||
) | ||
|
||
// Clean up idb entries exported to disk | ||
await Promise.all( | ||
idbKeys.map(async (key) => { | ||
const assetUrl = `${baseUrl}/${key}` | ||
|
||
try { | ||
const response = await fetch(assetUrl, {method: 'HEAD'}) | ||
if (response.ok) { | ||
await idb.del(key) | ||
} | ||
} catch (e) { | ||
notify.error( | ||
'Failed to access assets', | ||
`Failed to access assets at ${ | ||
project.config.assets?.baseUrl ?? '/' | ||
}. This is likely due to a CORS issue.`, | ||
) | ||
} | ||
}), | ||
) | ||
|
||
// A map for caching the assets outside of the db. We also need this to be able to retrieve idb asset urls synchronously. | ||
const assetsMap = new Map(await idb.entries<string, Blob>()) | ||
|
||
// A map for caching the object urls created from idb assets. | ||
const urlCache = new Map<Blob, string>() | ||
|
||
/** Gets idb aset url from asset blob */ | ||
const getUrlForAsset = (asset: Blob) => { | ||
if (urlCache.has(asset)) { | ||
return urlCache.get(asset)! | ||
} else { | ||
const url = URL.createObjectURL(asset) | ||
urlCache.set(asset, url) | ||
return url | ||
} | ||
} | ||
|
||
/** Gets idb asset url from id */ | ||
const getUrlForId = (assetId: string) => { | ||
const asset = assetsMap.get(assetId) | ||
if (!asset) { | ||
throw new Error(`Asset with id ${assetId} not found`) | ||
} | ||
return getUrlForAsset(asset) | ||
} | ||
|
||
return { | ||
getAssetUrl: (assetId: string) => { | ||
return assetsMap.has(assetId) | ||
? getUrlForId(assetId) | ||
: `${baseUrl}/${assetId}` | ||
}, | ||
createAsset: async (asset: File) => { | ||
const existingIDs = getAllPossibleAssetIDs(project) | ||
|
||
let sameSame = false | ||
|
||
if (existingIDs.includes(asset.name)) { | ||
let existingAsset: Blob | undefined | ||
try { | ||
existingAsset = | ||
assetsMap.get(asset.name) ?? | ||
(await fetch(`${baseUrl}/${asset.name}`).then((r) => | ||
r.ok ? r.blob() : undefined, | ||
)) | ||
} catch (e) { | ||
notify.error( | ||
'Failed to access assets', | ||
`Failed to access assets at ${ | ||
project.config.assets?.baseUrl ?? '/' | ||
}. This is likely due to a CORS issue.`, | ||
) | ||
|
||
return Promise.resolve(null) | ||
} | ||
|
||
if (existingAsset) { | ||
// @ts-ignore | ||
sameSame = await blobCompare.isEqual(asset, existingAsset) | ||
|
||
// if same same, we do nothing | ||
if (sameSame) { | ||
return asset.name | ||
// if different, we ask the user to pls rename | ||
} else { | ||
/** Initiates rename using a dialog. Returns a boolean indicating if the rename was succesful. */ | ||
const renameAsset = (text: string): boolean => { | ||
const newAssetName = prompt(text, asset.name) | ||
|
||
if (newAssetName === null) { | ||
// asset creation canceled | ||
return false | ||
} else if (newAssetName === '') { | ||
return renameAsset( | ||
'Asset name cannot be empty. Please choose a different file name for this asset.', | ||
) | ||
} else if (existingIDs.includes(newAssetName)) { | ||
console.log(existingIDs) | ||
return renameAsset( | ||
'An asset with this name already exists. Please choose a different file name for this asset.', | ||
) | ||
} | ||
|
||
// rename asset | ||
asset = new File([asset], newAssetName, {type: asset.type}) | ||
return true | ||
} | ||
|
||
// rename asset returns false if the user cancels the rename | ||
const success = renameAsset( | ||
'An asset with this name already exists. Please choose a different file name for this asset.', | ||
) | ||
|
||
if (!success) { | ||
return null | ||
} | ||
} | ||
} | ||
} | ||
|
||
assetsMap.set(asset.name, asset) | ||
await idb.set(asset.name, asset) | ||
return asset.name | ||
}, | ||
} | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import * as idb from 'idb-keyval' | ||
|
||
/** | ||
* Custom IDB keyval storage creator. Right now this exists solely as a more convenient way to use idb-keyval with a custom db name. | ||
* It also automatically prefixes the provided name with `theatrejs-` to avoid conflicts with other libraries. | ||
* | ||
* @param name - The name of the database | ||
* @returns An object with the same methods as idb-keyval, but with a custom database name | ||
*/ | ||
export const createStore = (name: string) => { | ||
const customStore = idb.createStore(`theatrejs-${name}`, 'default-store') | ||
|
||
return { | ||
set: (key: string, value: any) => idb.set(key, value, customStore), | ||
get: <T = any>(key: string) => idb.get<T>(key, customStore), | ||
del: (key: string) => idb.del(key, customStore), | ||
keys: <T extends IDBValidKey>() => idb.keys<T>(customStore), | ||
entries: <KeyType extends IDBValidKey, ValueType = any>() => | ||
idb.entries<KeyType, ValueType>(customStore), | ||
values: <T = any>() => idb.values<T>(customStore), | ||
} | ||
} |
Oops, something went wrong.